Complete recurrence editing and calendar preferences

This commit is contained in:
2026-07-31 05:46:50 +02:00
parent 1e063f51cc
commit f00cff1d8c
23 changed files with 2354 additions and 368 deletions
+147
View File
@@ -33,6 +33,8 @@ from govoplan_calendar.backend.schemas import (
CalendarEventCreateRequest,
CalendarEventDeltaResponse,
CalendarEventListResponse,
CalendarEventOccurrenceDeleteRequest,
CalendarEventOccurrenceUpdateRequest,
CalendarEventResponse,
CalendarEventUpdateRequest,
CalendarFreeBusyRequest,
@@ -49,6 +51,8 @@ from govoplan_calendar.backend.schemas import (
CalendarSyncSourceSyncRequest,
CalendarSyncSourceSyncResponse,
CalendarSyncSourceUpdateRequest,
CalendarViewPreferencesResponse,
CalendarViewPreferencesUpdateRequest,
)
from govoplan_calendar.backend.outbox import (
calendar_outbox_operation_response,
@@ -75,14 +79,17 @@ from govoplan_calendar.backend.service import (
delete_caldav_source,
delete_sync_source,
delete_event,
delete_event_occurrence,
discover_caldav_calendars,
event_response,
get_calendar_view_preferences,
get_event,
import_ics_event,
list_freebusy,
list_caldav_sources,
list_calendars,
list_events,
list_event_occurrences,
list_sync_sources,
normalize_datetime,
sync_due_sources,
@@ -93,6 +100,8 @@ from govoplan_calendar.backend.service import (
update_caldav_source,
update_sync_source,
update_event,
update_event_occurrence,
update_calendar_view_preferences,
)
from govoplan_core.db.session import get_session
@@ -743,10 +752,39 @@ def api_list_events(
calendar_id: str | None = None,
start_at: datetime | None = Query(default=None),
end_at: datetime | None = Query(default=None),
expand_recurring: bool = Query(default=False),
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:event:read")
if expand_recurring:
if start_at is None or end_at is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=(
"start_at and end_at are required when expanding "
"recurring events"
),
)
try:
occurrences = list_event_occurrences(
session,
tenant_id=principal.tenant_id,
calendar_id=calendar_id,
start_at=start_at,
end_at=end_at,
)
return CalendarEventListResponse(
events=[
CalendarEventResponse.model_validate(item)
for item in occurrences
]
)
except CalendarError as exc:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(exc),
) from exc
events = list_events(session, tenant_id=principal.tenant_id, calendar_id=calendar_id, start_at=start_at, end_at=end_at)
return CalendarEventListResponse(events=[_event_response(event) for event in events])
@@ -850,6 +888,70 @@ def api_update_event(
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
@router.patch(
"/events/{series_event_id}/occurrence",
response_model=CalendarEventResponse,
)
def api_update_event_occurrence(
series_event_id: str,
payload: CalendarEventOccurrenceUpdateRequest,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, CALENDAR_EVENT_WRITE_SCOPE)
try:
event = update_event_occurrence(
session,
tenant_id=principal.tenant_id,
user_id=principal.user.id,
series_event_id=series_event_id,
payload=payload,
)
session.commit()
session.refresh(event)
return _event_response(event)
except CalendarError as exc:
session.rollback()
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(exc),
) from exc
@router.delete(
"/events/{series_event_id}/occurrence",
response_model=CalendarEventResponse,
)
def api_delete_event_occurrence(
series_event_id: str,
payload: CalendarEventOccurrenceDeleteRequest,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_any_scope(
principal,
"calendar:event:delete",
CALENDAR_EVENT_WRITE_SCOPE,
)
try:
event = delete_event_occurrence(
session,
tenant_id=principal.tenant_id,
user_id=principal.user.id,
series_event_id=series_event_id,
recurrence_id=payload.recurrence_id,
)
session.commit()
session.refresh(event)
return _event_response(event)
except CalendarError as exc:
session.rollback()
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(exc),
) from exc
@router.delete("/events/{event_id}", status_code=status.HTTP_204_NO_CONTENT)
def api_delete_event(
event_id: str,
@@ -866,6 +968,51 @@ def api_delete_event(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
@router.get(
"/preferences/view",
response_model=CalendarViewPreferencesResponse,
)
def api_get_calendar_view_preferences(
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:event:read")
return CalendarViewPreferencesResponse.model_validate(
get_calendar_view_preferences(
session,
tenant_id=principal.tenant_id,
user_id=principal.user.id,
)
)
@router.patch(
"/preferences/view",
response_model=CalendarViewPreferencesResponse,
)
def api_update_calendar_view_preferences(
payload: CalendarViewPreferencesUpdateRequest,
principal: ApiPrincipal = Depends(get_api_principal),
session: Session = Depends(get_session),
):
_require_scope(principal, "calendar:event:read")
try:
response = update_calendar_view_preferences(
session,
tenant_id=principal.tenant_id,
user_id=principal.user.id,
payload=payload,
)
session.commit()
return CalendarViewPreferencesResponse.model_validate(response)
except CalendarError as exc:
session.rollback()
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(exc),
) from exc
@router.post("/events/import-ics", response_model=CalendarEventResponse, status_code=status.HTTP_201_CREATED)
def api_import_ics_event(
payload: CalendarIcsImportRequest,