[split-required] Split final 43 files (500-668 LOC) to complete refactoring
klausur-service (11 files): - cv_gutter_repair, ocr_pipeline_regression, upload_api - ocr_pipeline_sessions, smart_spell, nru_worksheet_generator - ocr_pipeline_overlays, mail/aggregator, zeugnis_api - cv_syllable_detect, self_rag backend-lehrer (17 files): - classroom_engine/suggestions, generators/quiz_generator - worksheets_api, llm_gateway/comparison, state_engine_api - classroom/models (→ 4 submodules), services/file_processor - alerts_agent/api/wizard+digests+routes, content_generators/pdf - classroom/routes/sessions, llm_gateway/inference - classroom_engine/analytics, auth/keycloak_auth - alerts_agent/processing/rule_engine, ai_processor/print_versions agent-core (5 files): - brain/memory_store, brain/knowledge_graph, brain/context_manager - orchestrator/supervisor, sessions/session_manager admin-lehrer (5 components): - GridOverlay, StepGridReview, DevOpsPipelineSidebar - DataFlowDiagram, sbom/wizard/page website (2 files): - DependencyMap, lehrer/abitur-archiv Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
205
backend-lehrer/classroom_engine/analytics_models.py
Normal file
205
backend-lehrer/classroom_engine/analytics_models.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
Analytics Models - Datenstrukturen fuer Classroom Analytics.
|
||||
|
||||
Enthaelt PhaseStatistics, SessionSummary, TeacherAnalytics, LessonReflection.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseStatistics:
|
||||
"""Statistik fuer eine einzelne Phase."""
|
||||
phase: str
|
||||
display_name: str
|
||||
|
||||
# Dauer-Metriken
|
||||
planned_duration_seconds: int
|
||||
actual_duration_seconds: int
|
||||
difference_seconds: int # positiv = laenger als geplant
|
||||
|
||||
# Overtime
|
||||
had_overtime: bool
|
||||
overtime_seconds: int = 0
|
||||
|
||||
# Erweiterungen
|
||||
was_extended: bool = False
|
||||
extension_minutes: int = 0
|
||||
|
||||
# Pausen
|
||||
pause_count: int = 0
|
||||
total_pause_seconds: int = 0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"phase": self.phase,
|
||||
"display_name": self.display_name,
|
||||
"planned_duration_seconds": self.planned_duration_seconds,
|
||||
"actual_duration_seconds": self.actual_duration_seconds,
|
||||
"difference_seconds": self.difference_seconds,
|
||||
"difference_formatted": self._format_difference(),
|
||||
"had_overtime": self.had_overtime,
|
||||
"overtime_seconds": self.overtime_seconds,
|
||||
"overtime_formatted": self._format_seconds(self.overtime_seconds),
|
||||
"was_extended": self.was_extended,
|
||||
"extension_minutes": self.extension_minutes,
|
||||
"pause_count": self.pause_count,
|
||||
"total_pause_seconds": self.total_pause_seconds,
|
||||
}
|
||||
|
||||
def _format_difference(self) -> str:
|
||||
prefix = "+" if self.difference_seconds >= 0 else ""
|
||||
return f"{prefix}{self._format_seconds(abs(self.difference_seconds))}"
|
||||
|
||||
def _format_seconds(self, seconds: int) -> str:
|
||||
mins = seconds // 60
|
||||
secs = seconds % 60
|
||||
return f"{mins:02d}:{secs:02d}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionSummary:
|
||||
"""Zusammenfassung einer Unterrichtsstunde."""
|
||||
session_id: str
|
||||
teacher_id: str
|
||||
class_id: str
|
||||
subject: str
|
||||
topic: Optional[str]
|
||||
date: datetime
|
||||
|
||||
total_duration_seconds: int
|
||||
planned_duration_seconds: int
|
||||
|
||||
phases_completed: int
|
||||
total_phases: int = 5
|
||||
phase_statistics: List[PhaseStatistics] = field(default_factory=list)
|
||||
|
||||
total_overtime_seconds: int = 0
|
||||
phases_with_overtime: int = 0
|
||||
|
||||
total_pause_count: int = 0
|
||||
total_pause_seconds: int = 0
|
||||
|
||||
reflection_notes: str = ""
|
||||
reflection_rating: Optional[int] = None
|
||||
key_learnings: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"teacher_id": self.teacher_id,
|
||||
"class_id": self.class_id,
|
||||
"subject": self.subject,
|
||||
"topic": self.topic,
|
||||
"date": self.date.isoformat() if self.date else None,
|
||||
"date_formatted": self._format_date(),
|
||||
"total_duration_seconds": self.total_duration_seconds,
|
||||
"total_duration_formatted": self._format_seconds(self.total_duration_seconds),
|
||||
"planned_duration_seconds": self.planned_duration_seconds,
|
||||
"planned_duration_formatted": self._format_seconds(self.planned_duration_seconds),
|
||||
"phases_completed": self.phases_completed,
|
||||
"total_phases": self.total_phases,
|
||||
"completion_percentage": round(self.phases_completed / self.total_phases * 100),
|
||||
"phase_statistics": [p.to_dict() for p in self.phase_statistics],
|
||||
"total_overtime_seconds": self.total_overtime_seconds,
|
||||
"total_overtime_formatted": self._format_seconds(self.total_overtime_seconds),
|
||||
"phases_with_overtime": self.phases_with_overtime,
|
||||
"total_pause_count": self.total_pause_count,
|
||||
"total_pause_seconds": self.total_pause_seconds,
|
||||
"reflection_notes": self.reflection_notes,
|
||||
"reflection_rating": self.reflection_rating,
|
||||
"key_learnings": self.key_learnings,
|
||||
}
|
||||
|
||||
def _format_seconds(self, seconds: int) -> str:
|
||||
mins = seconds // 60
|
||||
secs = seconds % 60
|
||||
return f"{mins:02d}:{secs:02d}"
|
||||
|
||||
def _format_date(self) -> str:
|
||||
if not self.date:
|
||||
return ""
|
||||
return self.date.strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeacherAnalytics:
|
||||
"""Aggregierte Statistiken fuer einen Lehrer."""
|
||||
teacher_id: str
|
||||
period_start: datetime
|
||||
period_end: datetime
|
||||
|
||||
total_sessions: int = 0
|
||||
completed_sessions: int = 0
|
||||
total_teaching_minutes: int = 0
|
||||
|
||||
avg_phase_durations: Dict[str, float] = field(default_factory=dict)
|
||||
|
||||
sessions_with_overtime: int = 0
|
||||
avg_overtime_seconds: float = 0
|
||||
most_overtime_phase: Optional[str] = None
|
||||
|
||||
avg_pause_count: float = 0
|
||||
avg_pause_duration_seconds: float = 0
|
||||
|
||||
subjects_taught: Dict[str, int] = field(default_factory=dict)
|
||||
classes_taught: Dict[str, int] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"teacher_id": self.teacher_id,
|
||||
"period_start": self.period_start.isoformat() if self.period_start else None,
|
||||
"period_end": self.period_end.isoformat() if self.period_end else None,
|
||||
"total_sessions": self.total_sessions,
|
||||
"completed_sessions": self.completed_sessions,
|
||||
"total_teaching_minutes": self.total_teaching_minutes,
|
||||
"total_teaching_hours": round(self.total_teaching_minutes / 60, 1),
|
||||
"avg_phase_durations": self.avg_phase_durations,
|
||||
"sessions_with_overtime": self.sessions_with_overtime,
|
||||
"overtime_percentage": round(self.sessions_with_overtime / max(self.total_sessions, 1) * 100),
|
||||
"avg_overtime_seconds": round(self.avg_overtime_seconds),
|
||||
"avg_overtime_formatted": self._format_seconds(int(self.avg_overtime_seconds)),
|
||||
"most_overtime_phase": self.most_overtime_phase,
|
||||
"avg_pause_count": round(self.avg_pause_count, 1),
|
||||
"avg_pause_duration_seconds": round(self.avg_pause_duration_seconds),
|
||||
"subjects_taught": self.subjects_taught,
|
||||
"classes_taught": self.classes_taught,
|
||||
}
|
||||
|
||||
def _format_seconds(self, seconds: int) -> str:
|
||||
mins = seconds // 60
|
||||
secs = seconds % 60
|
||||
return f"{mins:02d}:{secs:02d}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class LessonReflection:
|
||||
"""Post-Lesson Reflection (Feature)."""
|
||||
reflection_id: str
|
||||
session_id: str
|
||||
teacher_id: str
|
||||
|
||||
notes: str = ""
|
||||
overall_rating: Optional[int] = None
|
||||
what_worked: List[str] = field(default_factory=list)
|
||||
improvements: List[str] = field(default_factory=list)
|
||||
notes_for_next_lesson: str = ""
|
||||
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"reflection_id": self.reflection_id,
|
||||
"session_id": self.session_id,
|
||||
"teacher_id": self.teacher_id,
|
||||
"notes": self.notes,
|
||||
"overall_rating": self.overall_rating,
|
||||
"what_worked": self.what_worked,
|
||||
"improvements": self.improvements,
|
||||
"notes_for_next_lesson": self.notes_for_next_lesson,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
Reference in New Issue
Block a user