Files
breakpilot-lehrer/backend-lehrer/messenger_contacts.py
Benjamin Admin 34da9f4cda [split-required] Split 700-870 LOC files across all services
backend-lehrer (11 files):
- llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6)
- messenger_api.py (840 → 5), print_generator.py (824 → 5)
- unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4)
- llm_gateway/routes/edu_search_seeds.py (710 → 4)

klausur-service (12 files):
- ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4)
- legal_corpus_api.py (790 → 4), page_crop.py (758 → 3)
- mail/ai_service.py (747 → 4), github_crawler.py (767 → 3)
- trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4)
- dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4)

website (6 pages):
- audit-checklist (867 → 8), content (806 → 6)
- screen-flow (790 → 4), scraper (789 → 5)
- zeugnisse (776 → 5), modules (745 → 4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:01:18 +02:00

252 lines
8.2 KiB
Python

"""
Messenger API - Contact Routes.
CRUD, CSV import/export for contacts.
"""
import csv
import uuid
from io import StringIO
from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, HTTPException, UploadFile, File, Query
from fastapi.responses import StreamingResponse
from messenger_models import (
Contact,
ContactCreate,
ContactUpdate,
CSVImportResult,
)
from messenger_helpers import get_contacts, save_contacts
router = APIRouter(tags=["Messenger"])
# ==========================================
# CONTACTS ENDPOINTS
# ==========================================
@router.get("/contacts", response_model=List[Contact])
async def list_contacts(
role: Optional[str] = Query(None, description="Filter by role"),
class_name: Optional[str] = Query(None, description="Filter by class"),
search: Optional[str] = Query(None, description="Search in name/email")
):
"""Listet alle Kontakte auf."""
contacts = get_contacts()
# Filter anwenden
if role:
contacts = [c for c in contacts if c.get("role") == role]
if class_name:
contacts = [c for c in contacts if c.get("class_name") == class_name]
if search:
search_lower = search.lower()
contacts = [c for c in contacts if
search_lower in c.get("name", "").lower() or
search_lower in (c.get("email") or "").lower() or
search_lower in (c.get("student_name") or "").lower()]
return contacts
@router.post("/contacts", response_model=Contact)
async def create_contact(contact: ContactCreate):
"""Erstellt einen neuen Kontakt."""
contacts = get_contacts()
# Pruefen ob Email bereits existiert
if contact.email:
existing = [c for c in contacts if c.get("email") == contact.email]
if existing:
raise HTTPException(status_code=400, detail="Kontakt mit dieser Email existiert bereits")
now = datetime.utcnow().isoformat()
new_contact = {
"id": str(uuid.uuid4()),
"created_at": now,
"updated_at": now,
"online": False,
"last_seen": None,
**contact.dict()
}
contacts.append(new_contact)
save_contacts(contacts)
return new_contact
@router.get("/contacts/{contact_id}", response_model=Contact)
async def get_contact(contact_id: str):
"""Ruft einen einzelnen Kontakt ab."""
contacts = get_contacts()
contact = next((c for c in contacts if c["id"] == contact_id), None)
if not contact:
raise HTTPException(status_code=404, detail="Kontakt nicht gefunden")
return contact
@router.put("/contacts/{contact_id}", response_model=Contact)
async def update_contact(contact_id: str, update: ContactUpdate):
"""Aktualisiert einen Kontakt."""
contacts = get_contacts()
contact_idx = next((i for i, c in enumerate(contacts) if c["id"] == contact_id), None)
if contact_idx is None:
raise HTTPException(status_code=404, detail="Kontakt nicht gefunden")
update_data = update.dict(exclude_unset=True)
contacts[contact_idx].update(update_data)
contacts[contact_idx]["updated_at"] = datetime.utcnow().isoformat()
save_contacts(contacts)
return contacts[contact_idx]
@router.delete("/contacts/{contact_id}")
async def delete_contact(contact_id: str):
"""Loescht einen Kontakt."""
contacts = get_contacts()
contacts = [c for c in contacts if c["id"] != contact_id]
save_contacts(contacts)
return {"status": "deleted", "id": contact_id}
@router.post("/contacts/import", response_model=CSVImportResult)
async def import_contacts_csv(file: UploadFile = File(...)):
"""
Importiert Kontakte aus einer CSV-Datei.
Erwartete Spalten:
- name (required)
- email
- phone
- role (parent/teacher/staff/student)
- student_name
- class_name
- notes
- tags (komma-separiert)
"""
if not file.filename.endswith('.csv'):
raise HTTPException(status_code=400, detail="Nur CSV-Dateien werden unterstuetzt")
content = await file.read()
try:
text = content.decode('utf-8')
except UnicodeDecodeError:
text = content.decode('latin-1')
contacts = get_contacts()
existing_emails = {c.get("email") for c in contacts if c.get("email")}
imported = []
skipped = 0
errors = []
reader = csv.DictReader(StringIO(text), delimiter=';') # Deutsche CSV meist mit Semikolon
if not reader.fieldnames or 'name' not in [f.lower() for f in reader.fieldnames]:
# Versuche mit Komma
reader = csv.DictReader(StringIO(text), delimiter=',')
for row_num, row in enumerate(reader, start=2):
try:
# Normalisiere Spaltennamen
row = {k.lower().strip(): v.strip() if v else "" for k, v in row.items()}
name = row.get('name') or row.get('kontakt') or row.get('elternname')
if not name:
errors.append(f"Zeile {row_num}: Name fehlt")
skipped += 1
continue
email = row.get('email') or row.get('e-mail') or row.get('mail')
if email and email in existing_emails:
errors.append(f"Zeile {row_num}: Email {email} existiert bereits")
skipped += 1
continue
now = datetime.utcnow().isoformat()
tags_str = row.get('tags') or row.get('kategorien') or ""
tags = [t.strip() for t in tags_str.split(',') if t.strip()]
# Matrix-ID und preferred_channel auslesen
matrix_id = row.get('matrix_id') or row.get('matrix') or None
preferred_channel = row.get('preferred_channel') or row.get('kanal') or "email"
if preferred_channel not in ["email", "matrix", "pwa"]:
preferred_channel = "email"
new_contact = {
"id": str(uuid.uuid4()),
"name": name,
"email": email if email else None,
"phone": row.get('phone') or row.get('telefon') or row.get('tel'),
"role": row.get('role') or row.get('rolle') or "parent",
"student_name": row.get('student_name') or row.get('schueler') or row.get('kind'),
"class_name": row.get('class_name') or row.get('klasse'),
"notes": row.get('notes') or row.get('notizen') or row.get('bemerkungen'),
"tags": tags,
"matrix_id": matrix_id if matrix_id else None,
"preferred_channel": preferred_channel,
"created_at": now,
"updated_at": now,
"online": False,
"last_seen": None
}
contacts.append(new_contact)
imported.append(new_contact)
if email:
existing_emails.add(email)
except Exception as e:
errors.append(f"Zeile {row_num}: {str(e)}")
skipped += 1
save_contacts(contacts)
return CSVImportResult(
imported=len(imported),
skipped=skipped,
errors=errors[:20], # Maximal 20 Fehler zurueckgeben
contacts=imported
)
@router.get("/contacts/export/csv")
async def export_contacts_csv():
"""Exportiert alle Kontakte als CSV."""
contacts = get_contacts()
output = StringIO()
fieldnames = ['name', 'email', 'phone', 'role', 'student_name', 'class_name', 'notes', 'tags', 'matrix_id', 'preferred_channel']
writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=';')
writer.writeheader()
for contact in contacts:
writer.writerow({
'name': contact.get('name', ''),
'email': contact.get('email', ''),
'phone': contact.get('phone', ''),
'role': contact.get('role', ''),
'student_name': contact.get('student_name', ''),
'class_name': contact.get('class_name', ''),
'notes': contact.get('notes', ''),
'tags': ','.join(contact.get('tags', [])),
'matrix_id': contact.get('matrix_id', ''),
'preferred_channel': contact.get('preferred_channel', 'email')
})
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=kontakte.csv"}
)