klausur-service (7 monoliths): - grid_editor_helpers.py (1,737 → 5 files: columns, filters, headers, zones) - cv_cell_grid.py (1,675 → 7 files: build, legacy, streaming, merge, vocab) - worksheet_editor_api.py (1,305 → 4 files: models, AI, reconstruct, routes) - legal_corpus_ingestion.py (1,280 → 3 files: registry, chunking, ingestion) - cv_review.py (1,248 → 4 files: pipeline, spell, LLM, barrel) - cv_preprocessing.py (1,166 → 3 files: deskew, dewarp, barrel) - rbac.py, admin_api.py, routes/eh.py remain (next batch) backend-lehrer (1 monolith): - classroom_engine/repository.py (1,705 → 7 files by domain) All re-export barrels preserve backward compatibility. Zero import errors verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
454 lines
16 KiB
Python
454 lines
16 KiB
Python
"""
|
|
Teacher Context, Schoolyear Event & Recurring Routine Repositories.
|
|
|
|
CRUD-Operationen fuer Schuljahres-Kontext (Phase 8).
|
|
"""
|
|
from datetime import datetime
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
from sqlalchemy.orm import Session as DBSession
|
|
|
|
from .context_models import (
|
|
TeacherContextDB, SchoolyearEventDB, RecurringRoutineDB,
|
|
MacroPhaseEnum, EventTypeEnum, EventStatusEnum,
|
|
RoutineTypeEnum, RecurrencePatternEnum,
|
|
FEDERAL_STATES, SCHOOL_TYPES,
|
|
)
|
|
|
|
|
|
class TeacherContextRepository:
|
|
"""Repository fuer Lehrer-Kontext CRUD-Operationen (Phase 8)."""
|
|
|
|
def __init__(self, db: DBSession):
|
|
self.db = db
|
|
|
|
# ==================== CREATE / GET-OR-CREATE ====================
|
|
|
|
def get_or_create(self, teacher_id: str) -> TeacherContextDB:
|
|
"""
|
|
Holt den Kontext eines Lehrers oder erstellt einen neuen.
|
|
|
|
Args:
|
|
teacher_id: ID des Lehrers
|
|
|
|
Returns:
|
|
TeacherContextDB Model
|
|
"""
|
|
context = self.get_by_teacher_id(teacher_id)
|
|
if context:
|
|
return context
|
|
|
|
# Neuen Kontext erstellen
|
|
from uuid import uuid4
|
|
context = TeacherContextDB(
|
|
id=str(uuid4()),
|
|
teacher_id=teacher_id,
|
|
macro_phase=MacroPhaseEnum.ONBOARDING,
|
|
)
|
|
self.db.add(context)
|
|
self.db.commit()
|
|
self.db.refresh(context)
|
|
return context
|
|
|
|
# ==================== READ ====================
|
|
|
|
def get_by_teacher_id(self, teacher_id: str) -> Optional[TeacherContextDB]:
|
|
"""Holt den Kontext eines Lehrers."""
|
|
return self.db.query(TeacherContextDB).filter(
|
|
TeacherContextDB.teacher_id == teacher_id
|
|
).first()
|
|
|
|
# ==================== UPDATE ====================
|
|
|
|
def update_context(
|
|
self,
|
|
teacher_id: str,
|
|
federal_state: str = None,
|
|
school_type: str = None,
|
|
schoolyear: str = None,
|
|
schoolyear_start: datetime = None,
|
|
macro_phase: str = None,
|
|
current_week: int = None,
|
|
) -> Optional[TeacherContextDB]:
|
|
"""Aktualisiert den Kontext eines Lehrers."""
|
|
context = self.get_or_create(teacher_id)
|
|
|
|
if federal_state is not None:
|
|
context.federal_state = federal_state
|
|
if school_type is not None:
|
|
context.school_type = school_type
|
|
if schoolyear is not None:
|
|
context.schoolyear = schoolyear
|
|
if schoolyear_start is not None:
|
|
context.schoolyear_start = schoolyear_start
|
|
if macro_phase is not None:
|
|
context.macro_phase = MacroPhaseEnum(macro_phase)
|
|
if current_week is not None:
|
|
context.current_week = current_week
|
|
|
|
self.db.commit()
|
|
self.db.refresh(context)
|
|
return context
|
|
|
|
def complete_onboarding(self, teacher_id: str) -> TeacherContextDB:
|
|
"""Markiert Onboarding als abgeschlossen."""
|
|
context = self.get_or_create(teacher_id)
|
|
context.onboarding_completed = True
|
|
context.macro_phase = MacroPhaseEnum.SCHULJAHRESSTART
|
|
self.db.commit()
|
|
self.db.refresh(context)
|
|
return context
|
|
|
|
def update_flags(
|
|
self,
|
|
teacher_id: str,
|
|
has_classes: bool = None,
|
|
has_schedule: bool = None,
|
|
is_exam_period: bool = None,
|
|
is_before_holidays: bool = None,
|
|
) -> TeacherContextDB:
|
|
"""Aktualisiert die Status-Flags eines Kontexts."""
|
|
context = self.get_or_create(teacher_id)
|
|
|
|
if has_classes is not None:
|
|
context.has_classes = has_classes
|
|
if has_schedule is not None:
|
|
context.has_schedule = has_schedule
|
|
if is_exam_period is not None:
|
|
context.is_exam_period = is_exam_period
|
|
if is_before_holidays is not None:
|
|
context.is_before_holidays = is_before_holidays
|
|
|
|
self.db.commit()
|
|
self.db.refresh(context)
|
|
return context
|
|
|
|
def to_dict(self, context: TeacherContextDB) -> Dict[str, Any]:
|
|
"""Konvertiert DB-Model zu Dictionary."""
|
|
return {
|
|
"id": context.id,
|
|
"teacher_id": context.teacher_id,
|
|
"school": {
|
|
"federal_state": context.federal_state,
|
|
"federal_state_name": FEDERAL_STATES.get(context.federal_state, ""),
|
|
"school_type": context.school_type,
|
|
"school_type_name": SCHOOL_TYPES.get(context.school_type, ""),
|
|
},
|
|
"school_year": {
|
|
"id": context.schoolyear,
|
|
"start": context.schoolyear_start.isoformat() if context.schoolyear_start else None,
|
|
"current_week": context.current_week,
|
|
},
|
|
"macro_phase": {
|
|
"id": context.macro_phase.value,
|
|
"label": self._get_phase_label(context.macro_phase),
|
|
},
|
|
"flags": {
|
|
"onboarding_completed": context.onboarding_completed,
|
|
"has_classes": context.has_classes,
|
|
"has_schedule": context.has_schedule,
|
|
"is_exam_period": context.is_exam_period,
|
|
"is_before_holidays": context.is_before_holidays,
|
|
},
|
|
"created_at": context.created_at.isoformat() if context.created_at else None,
|
|
"updated_at": context.updated_at.isoformat() if context.updated_at else None,
|
|
}
|
|
|
|
def _get_phase_label(self, phase: MacroPhaseEnum) -> str:
|
|
"""Gibt den Anzeigenamen einer Makro-Phase zurueck."""
|
|
labels = {
|
|
MacroPhaseEnum.ONBOARDING: "Einrichtung",
|
|
MacroPhaseEnum.SCHULJAHRESSTART: "Schuljahresstart",
|
|
MacroPhaseEnum.UNTERRICHTSAUFBAU: "Unterrichtsaufbau",
|
|
MacroPhaseEnum.LEISTUNGSPHASE_1: "Leistungsphase 1",
|
|
MacroPhaseEnum.HALBJAHRESABSCHLUSS: "Halbjahresabschluss",
|
|
MacroPhaseEnum.LEISTUNGSPHASE_2: "Leistungsphase 2",
|
|
MacroPhaseEnum.JAHRESABSCHLUSS: "Jahresabschluss",
|
|
}
|
|
return labels.get(phase, phase.value)
|
|
|
|
|
|
class SchoolyearEventRepository:
|
|
"""Repository fuer Schuljahr-Events (Phase 8)."""
|
|
|
|
def __init__(self, db: DBSession):
|
|
self.db = db
|
|
|
|
def create(
|
|
self,
|
|
teacher_id: str,
|
|
title: str,
|
|
start_date: datetime,
|
|
event_type: str = "other",
|
|
end_date: datetime = None,
|
|
class_id: str = None,
|
|
subject: str = None,
|
|
description: str = "",
|
|
needs_preparation: bool = True,
|
|
reminder_days_before: int = 7,
|
|
extra_data: Dict[str, Any] = None,
|
|
) -> SchoolyearEventDB:
|
|
"""Erstellt ein neues Schuljahr-Event."""
|
|
from uuid import uuid4
|
|
event = SchoolyearEventDB(
|
|
id=str(uuid4()),
|
|
teacher_id=teacher_id,
|
|
title=title,
|
|
event_type=EventTypeEnum(event_type),
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
class_id=class_id,
|
|
subject=subject,
|
|
description=description,
|
|
needs_preparation=needs_preparation,
|
|
reminder_days_before=reminder_days_before,
|
|
extra_data=extra_data or {},
|
|
)
|
|
self.db.add(event)
|
|
self.db.commit()
|
|
self.db.refresh(event)
|
|
return event
|
|
|
|
def get_by_id(self, event_id: str) -> Optional[SchoolyearEventDB]:
|
|
"""Holt ein Event nach ID."""
|
|
return self.db.query(SchoolyearEventDB).filter(
|
|
SchoolyearEventDB.id == event_id
|
|
).first()
|
|
|
|
def get_by_teacher(
|
|
self,
|
|
teacher_id: str,
|
|
status: str = None,
|
|
event_type: str = None,
|
|
limit: int = 50,
|
|
) -> List[SchoolyearEventDB]:
|
|
"""Holt Events eines Lehrers."""
|
|
query = self.db.query(SchoolyearEventDB).filter(
|
|
SchoolyearEventDB.teacher_id == teacher_id
|
|
)
|
|
if status:
|
|
query = query.filter(SchoolyearEventDB.status == EventStatusEnum(status))
|
|
if event_type:
|
|
query = query.filter(SchoolyearEventDB.event_type == EventTypeEnum(event_type))
|
|
|
|
return query.order_by(SchoolyearEventDB.start_date).limit(limit).all()
|
|
|
|
def get_upcoming(
|
|
self,
|
|
teacher_id: str,
|
|
days: int = 30,
|
|
limit: int = 10,
|
|
) -> List[SchoolyearEventDB]:
|
|
"""Holt anstehende Events der naechsten X Tage."""
|
|
from datetime import timedelta
|
|
now = datetime.utcnow()
|
|
end = now + timedelta(days=days)
|
|
|
|
return self.db.query(SchoolyearEventDB).filter(
|
|
SchoolyearEventDB.teacher_id == teacher_id,
|
|
SchoolyearEventDB.start_date >= now,
|
|
SchoolyearEventDB.start_date <= end,
|
|
SchoolyearEventDB.status != EventStatusEnum.CANCELLED,
|
|
).order_by(SchoolyearEventDB.start_date).limit(limit).all()
|
|
|
|
def update_status(
|
|
self,
|
|
event_id: str,
|
|
status: str,
|
|
preparation_done: bool = None,
|
|
) -> Optional[SchoolyearEventDB]:
|
|
"""Aktualisiert den Status eines Events."""
|
|
event = self.get_by_id(event_id)
|
|
if not event:
|
|
return None
|
|
|
|
event.status = EventStatusEnum(status)
|
|
if preparation_done is not None:
|
|
event.preparation_done = preparation_done
|
|
|
|
self.db.commit()
|
|
self.db.refresh(event)
|
|
return event
|
|
|
|
def delete(self, event_id: str) -> bool:
|
|
"""Loescht ein Event."""
|
|
event = self.get_by_id(event_id)
|
|
if not event:
|
|
return False
|
|
self.db.delete(event)
|
|
self.db.commit()
|
|
return True
|
|
|
|
def to_dict(self, event: SchoolyearEventDB) -> Dict[str, Any]:
|
|
"""Konvertiert DB-Model zu Dictionary."""
|
|
return {
|
|
"id": event.id,
|
|
"teacher_id": event.teacher_id,
|
|
"event_type": event.event_type.value,
|
|
"title": event.title,
|
|
"description": event.description,
|
|
"start_date": event.start_date.isoformat() if event.start_date else None,
|
|
"end_date": event.end_date.isoformat() if event.end_date else None,
|
|
"class_id": event.class_id,
|
|
"subject": event.subject,
|
|
"status": event.status.value,
|
|
"needs_preparation": event.needs_preparation,
|
|
"preparation_done": event.preparation_done,
|
|
"reminder_days_before": event.reminder_days_before,
|
|
"extra_data": event.extra_data,
|
|
"created_at": event.created_at.isoformat() if event.created_at else None,
|
|
}
|
|
|
|
|
|
class RecurringRoutineRepository:
|
|
"""Repository fuer wiederkehrende Routinen (Phase 8)."""
|
|
|
|
def __init__(self, db: DBSession):
|
|
self.db = db
|
|
|
|
def create(
|
|
self,
|
|
teacher_id: str,
|
|
title: str,
|
|
routine_type: str = "other",
|
|
recurrence_pattern: str = "weekly",
|
|
day_of_week: int = None,
|
|
day_of_month: int = None,
|
|
time_of_day: str = None, # Format: "14:00"
|
|
duration_minutes: int = 60,
|
|
description: str = "",
|
|
valid_from: datetime = None,
|
|
valid_until: datetime = None,
|
|
) -> RecurringRoutineDB:
|
|
"""Erstellt eine neue wiederkehrende Routine."""
|
|
from uuid import uuid4
|
|
from datetime import time as dt_time
|
|
|
|
time_obj = None
|
|
if time_of_day:
|
|
parts = time_of_day.split(":")
|
|
time_obj = dt_time(int(parts[0]), int(parts[1]))
|
|
|
|
routine = RecurringRoutineDB(
|
|
id=str(uuid4()),
|
|
teacher_id=teacher_id,
|
|
title=title,
|
|
routine_type=RoutineTypeEnum(routine_type),
|
|
recurrence_pattern=RecurrencePatternEnum(recurrence_pattern),
|
|
day_of_week=day_of_week,
|
|
day_of_month=day_of_month,
|
|
time_of_day=time_obj,
|
|
duration_minutes=duration_minutes,
|
|
description=description,
|
|
valid_from=valid_from,
|
|
valid_until=valid_until,
|
|
)
|
|
self.db.add(routine)
|
|
self.db.commit()
|
|
self.db.refresh(routine)
|
|
return routine
|
|
|
|
def get_by_id(self, routine_id: str) -> Optional[RecurringRoutineDB]:
|
|
"""Holt eine Routine nach ID."""
|
|
return self.db.query(RecurringRoutineDB).filter(
|
|
RecurringRoutineDB.id == routine_id
|
|
).first()
|
|
|
|
def get_by_teacher(
|
|
self,
|
|
teacher_id: str,
|
|
is_active: bool = True,
|
|
routine_type: str = None,
|
|
) -> List[RecurringRoutineDB]:
|
|
"""Holt Routinen eines Lehrers."""
|
|
query = self.db.query(RecurringRoutineDB).filter(
|
|
RecurringRoutineDB.teacher_id == teacher_id
|
|
)
|
|
if is_active is not None:
|
|
query = query.filter(RecurringRoutineDB.is_active == is_active)
|
|
if routine_type:
|
|
query = query.filter(RecurringRoutineDB.routine_type == RoutineTypeEnum(routine_type))
|
|
|
|
return query.all()
|
|
|
|
def get_today(self, teacher_id: str) -> List[RecurringRoutineDB]:
|
|
"""Holt Routinen die heute stattfinden."""
|
|
today = datetime.utcnow()
|
|
day_of_week = today.weekday() # 0 = Montag
|
|
day_of_month = today.day
|
|
|
|
routines = self.get_by_teacher(teacher_id, is_active=True)
|
|
today_routines = []
|
|
|
|
for routine in routines:
|
|
if routine.recurrence_pattern == RecurrencePatternEnum.DAILY:
|
|
today_routines.append(routine)
|
|
elif routine.recurrence_pattern == RecurrencePatternEnum.WEEKLY:
|
|
if routine.day_of_week == day_of_week:
|
|
today_routines.append(routine)
|
|
elif routine.recurrence_pattern == RecurrencePatternEnum.BIWEEKLY:
|
|
# Vereinfacht: Pruefen ob Tag passt (echte Logik braucht Startdatum)
|
|
if routine.day_of_week == day_of_week:
|
|
today_routines.append(routine)
|
|
elif routine.recurrence_pattern == RecurrencePatternEnum.MONTHLY:
|
|
if routine.day_of_month == day_of_month:
|
|
today_routines.append(routine)
|
|
|
|
return today_routines
|
|
|
|
def update(
|
|
self,
|
|
routine_id: str,
|
|
title: str = None,
|
|
is_active: bool = None,
|
|
day_of_week: int = None,
|
|
time_of_day: str = None,
|
|
) -> Optional[RecurringRoutineDB]:
|
|
"""Aktualisiert eine Routine."""
|
|
routine = self.get_by_id(routine_id)
|
|
if not routine:
|
|
return None
|
|
|
|
if title is not None:
|
|
routine.title = title
|
|
if is_active is not None:
|
|
routine.is_active = is_active
|
|
if day_of_week is not None:
|
|
routine.day_of_week = day_of_week
|
|
if time_of_day is not None:
|
|
from datetime import time as dt_time
|
|
parts = time_of_day.split(":")
|
|
routine.time_of_day = dt_time(int(parts[0]), int(parts[1]))
|
|
|
|
self.db.commit()
|
|
self.db.refresh(routine)
|
|
return routine
|
|
|
|
def delete(self, routine_id: str) -> bool:
|
|
"""Loescht eine Routine."""
|
|
routine = self.get_by_id(routine_id)
|
|
if not routine:
|
|
return False
|
|
self.db.delete(routine)
|
|
self.db.commit()
|
|
return True
|
|
|
|
def to_dict(self, routine: RecurringRoutineDB) -> Dict[str, Any]:
|
|
"""Konvertiert DB-Model zu Dictionary."""
|
|
return {
|
|
"id": routine.id,
|
|
"teacher_id": routine.teacher_id,
|
|
"routine_type": routine.routine_type.value,
|
|
"title": routine.title,
|
|
"description": routine.description,
|
|
"recurrence_pattern": routine.recurrence_pattern.value,
|
|
"day_of_week": routine.day_of_week,
|
|
"day_of_month": routine.day_of_month,
|
|
"time_of_day": routine.time_of_day.isoformat() if routine.time_of_day else None,
|
|
"duration_minutes": routine.duration_minutes,
|
|
"is_active": routine.is_active,
|
|
"valid_from": routine.valid_from.isoformat() if routine.valid_from else None,
|
|
"valid_until": routine.valid_until.isoformat() if routine.valid_until else None,
|
|
"created_at": routine.created_at.isoformat() if routine.created_at else None,
|
|
}
|