[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>
This commit is contained in:
251
backend-lehrer/messenger_contacts.py
Normal file
251
backend-lehrer/messenger_contacts.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
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"}
|
||||
)
|
||||
Reference in New Issue
Block a user