feat: Kalender-Sharing, Gruppen, iCal Import/Export & Ersteller (Server)

Kollaborations-Features ausschliesslich fuer lokale Kalender:

- Sharing: calendar_shares-Tabelle, GET/POST/DELETE /api/local/calendars/{id}/shares
  (nur Besitzer), GET /api/users/directory, geteilte Kalender in
  GET /api/local/calendars (shared_by/permission/owned) und im Merge-Read.
- Gruppen: groups/group_members/group_calendars + /api/groups-Router inkl.
  kombinierter Ansicht /api/groups/{id}/combined (owner + is_group_event).
- Ersteller: local_events.creator_id (serverseitig gesetzt) + creator_name_external
  aus ORGANIZER; creator-Feld in allen lokalen Event-Responses.
- Private-Flag: local_events.is_private + user_settings.private_event_visibility
  (hidden|busy), Filterung in der Gruppenansicht.
- iCal Import/Export: ical_io.py, POST /api/local/calendars/{id}/import,
  POST /api/local/import, GET /api/local/calendars/{id}/export.
- Zentraler Berechtigungs-Helper (permissions.py) und gemeinsamer Event-Dict-
  Builder (local_events_util.py) ersetzen die Nur-Besitzer-Filter.
- pytest-Suite (12 Tests) fuer Sharing, Gruppen, Parser, Private-Filterung.

Additiv & rueckwaertskompatibel; Migrationen in main.py._migrate().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-05-31 16:05:18 +02:00
parent 362cc7212c
commit 32268a18b2
13 changed files with 1582 additions and 173 deletions

View File

@@ -1,6 +1,6 @@
from typing import Optional
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
@@ -27,6 +27,7 @@ class SettingsUpdate(BaseModel):
text_color: Optional[str] = None
line_color: Optional[str] = None
bg_color: Optional[str] = None
private_event_visibility: Optional[str] = None
def _settings_dict(s: models.UserSettings) -> dict:
@@ -46,6 +47,7 @@ def _settings_dict(s: models.UserSettings) -> dict:
"text_color": s.text_color,
"line_color": s.line_color,
"bg_color": s.bg_color,
"private_event_visibility": s.private_event_visibility or "busy",
}
@@ -82,6 +84,9 @@ def update_settings(
settings = models.UserSettings(user_id=current_user.id)
db.add(settings)
if data.private_event_visibility is not None and data.private_event_visibility not in ("hidden", "busy"):
raise HTTPException(422, "private_event_visibility must be 'hidden' or 'busy'")
# For these three override colours, an explicit null is meaningful
# ("reset to default") and must be persisted as NULL. All other fields
# keep the previous behaviour where a null/missing value is ignored.