Initialize GovOPlaN calendar module
This commit is contained in:
244
src/govoplan_calendar/backend/router.py
Normal file
244
src/govoplan_calendar/backend/router.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_calendar.backend.ical import ICalendarError, event_to_ics, http_last_modified
|
||||
from govoplan_calendar.backend.schemas import (
|
||||
CalendarCollectionCreateRequest,
|
||||
CalendarCollectionListResponse,
|
||||
CalendarCollectionResponse,
|
||||
CalendarCollectionUpdateRequest,
|
||||
CalendarEventCreateRequest,
|
||||
CalendarEventListResponse,
|
||||
CalendarEventResponse,
|
||||
CalendarEventUpdateRequest,
|
||||
CalendarIcsImportRequest,
|
||||
)
|
||||
from govoplan_calendar.backend.service import (
|
||||
CalendarError,
|
||||
calendar_response,
|
||||
create_calendar,
|
||||
create_event,
|
||||
delete_calendar,
|
||||
delete_event,
|
||||
event_response,
|
||||
get_event,
|
||||
import_ics_event,
|
||||
list_calendars,
|
||||
list_events,
|
||||
update_calendar,
|
||||
update_event,
|
||||
)
|
||||
from govoplan_core.db.session import get_session
|
||||
|
||||
router = APIRouter(prefix="/calendar", tags=["calendar"])
|
||||
|
||||
|
||||
def _require_scope(principal: ApiPrincipal, scope: str) -> None:
|
||||
if not has_scope(principal, scope):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Missing scope: {scope}")
|
||||
|
||||
|
||||
def _require_any_scope(principal: ApiPrincipal, *scopes: str) -> None:
|
||||
if not any(has_scope(principal, scope) for scope in scopes):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Requires one of: " + ", ".join(scopes))
|
||||
|
||||
|
||||
def _calendar_response(calendar) -> CalendarCollectionResponse:
|
||||
return CalendarCollectionResponse.model_validate(calendar_response(calendar))
|
||||
|
||||
|
||||
def _event_response(event) -> CalendarEventResponse:
|
||||
return CalendarEventResponse.model_validate(event_response(event))
|
||||
|
||||
|
||||
@router.get("/calendars", response_model=CalendarCollectionListResponse)
|
||||
def api_list_calendars(
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:calendar:read")
|
||||
calendars = list_calendars(session, tenant_id=principal.tenant_id, user_id=principal.user.id)
|
||||
session.commit()
|
||||
return CalendarCollectionListResponse(calendars=[_calendar_response(calendar) for calendar in calendars])
|
||||
|
||||
|
||||
@router.post("/calendars", response_model=CalendarCollectionResponse, status_code=status.HTTP_201_CREATED)
|
||||
def api_create_calendar(
|
||||
payload: CalendarCollectionCreateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:calendar:write")
|
||||
try:
|
||||
calendar = create_calendar(session, tenant_id=principal.tenant_id, user_id=principal.user.id, payload=payload)
|
||||
session.commit()
|
||||
session.refresh(calendar)
|
||||
return _calendar_response(calendar)
|
||||
except CalendarError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.patch("/calendars/{calendar_id}", response_model=CalendarCollectionResponse)
|
||||
def api_update_calendar(
|
||||
calendar_id: str,
|
||||
payload: CalendarCollectionUpdateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:calendar:write")
|
||||
try:
|
||||
calendar = update_calendar(session, tenant_id=principal.tenant_id, calendar_id=calendar_id, payload=payload)
|
||||
session.commit()
|
||||
session.refresh(calendar)
|
||||
return _calendar_response(calendar)
|
||||
except CalendarError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/calendars/{calendar_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def api_delete_calendar(
|
||||
calendar_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:calendar:admin")
|
||||
try:
|
||||
delete_calendar(session, tenant_id=principal.tenant_id, calendar_id=calendar_id)
|
||||
session.commit()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
except CalendarError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/events", response_model=CalendarEventListResponse)
|
||||
def api_list_events(
|
||||
calendar_id: str | None = None,
|
||||
start_at: datetime | None = Query(default=None),
|
||||
end_at: datetime | None = Query(default=None),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:event:read")
|
||||
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])
|
||||
|
||||
|
||||
@router.post("/events", response_model=CalendarEventResponse, status_code=status.HTTP_201_CREATED)
|
||||
def api_create_event(
|
||||
payload: CalendarEventCreateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:event:write")
|
||||
try:
|
||||
event = create_event(session, tenant_id=principal.tenant_id, user_id=principal.user.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_ENTITY, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/events/{event_id}", response_model=CalendarEventResponse)
|
||||
def api_get_event(
|
||||
event_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:event:read")
|
||||
try:
|
||||
return _event_response(get_event(session, tenant_id=principal.tenant_id, event_id=event_id))
|
||||
except CalendarError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.patch("/events/{event_id}", response_model=CalendarEventResponse)
|
||||
def api_update_event(
|
||||
event_id: str,
|
||||
payload: CalendarEventUpdateRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:event:write")
|
||||
try:
|
||||
event = update_event(session, tenant_id=principal.tenant_id, user_id=principal.user.id, event_id=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_ENTITY, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/events/{event_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def api_delete_event(
|
||||
event_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_any_scope(principal, "calendar:event:delete", "calendar:event:write")
|
||||
try:
|
||||
delete_event(session, tenant_id=principal.tenant_id, event_id=event_id)
|
||||
session.commit()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
except CalendarError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, 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,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:event:import")
|
||||
try:
|
||||
event = import_ics_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=principal.user.id,
|
||||
calendar_id=payload.calendar_id,
|
||||
ics=payload.ics,
|
||||
upsert=payload.upsert,
|
||||
source_kind=payload.source_kind,
|
||||
source_href=payload.source_href,
|
||||
etag=payload.etag,
|
||||
)
|
||||
session.commit()
|
||||
session.refresh(event)
|
||||
return _event_response(event)
|
||||
except (CalendarError, ICalendarError) as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/events/{event_id}/ics")
|
||||
def api_export_ics_event(
|
||||
event_id: str,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
_require_scope(principal, "calendar:event:export")
|
||||
try:
|
||||
event = get_event(session, tenant_id=principal.tenant_id, event_id=event_id)
|
||||
except CalendarError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||||
ics = event.raw_ics or event_to_ics(event)
|
||||
return Response(
|
||||
content=ics,
|
||||
media_type="text/calendar; charset=utf-8",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{event.uid}.ics"',
|
||||
"Last-Modified": http_last_modified(event.updated_at),
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user