136 lines
4.2 KiB
Python
136 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
|
from govoplan_core.db.session import get_session
|
|
from govoplan_dashboard.backend.db.models import DashboardLayout
|
|
from govoplan_dashboard.backend.permissions import READ_SCOPE
|
|
from govoplan_dashboard.backend.schemas import (
|
|
DashboardLayoutResponse,
|
|
DashboardLayoutUpdateRequest,
|
|
)
|
|
from govoplan_dashboard.backend.service import (
|
|
DashboardLayoutConflict,
|
|
DashboardLayoutLimitExceeded,
|
|
delete_dashboard_layout,
|
|
get_dashboard_layout,
|
|
normalize_view_id,
|
|
save_dashboard_layout,
|
|
)
|
|
|
|
|
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
|
|
|
|
def _require_read(principal: ApiPrincipal) -> None:
|
|
if not has_scope(principal, READ_SCOPE):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Missing required scope: {READ_SCOPE}",
|
|
)
|
|
|
|
|
|
def _response(
|
|
layout: DashboardLayout | None,
|
|
*,
|
|
view_id: str | None,
|
|
) -> DashboardLayoutResponse:
|
|
if layout is None:
|
|
return DashboardLayoutResponse(
|
|
exists=False,
|
|
view_id=normalize_view_id(view_id),
|
|
)
|
|
return DashboardLayoutResponse(
|
|
exists=True,
|
|
view_id=layout.view_id,
|
|
layout_version=layout.layout_version,
|
|
revision=layout.revision,
|
|
placements=layout.placements,
|
|
known_widget_ids=layout.known_widget_ids,
|
|
updated_at=layout.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("/layout", response_model=DashboardLayoutResponse)
|
|
def api_get_dashboard_layout(
|
|
view_id: str | None = Query(default=None, max_length=255),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(get_api_principal),
|
|
) -> DashboardLayoutResponse:
|
|
_require_read(principal)
|
|
return _response(
|
|
get_dashboard_layout(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
account_id=principal.account_id,
|
|
view_id=view_id,
|
|
),
|
|
view_id=view_id,
|
|
)
|
|
|
|
|
|
@router.put("/layout", response_model=DashboardLayoutResponse)
|
|
def api_save_dashboard_layout(
|
|
payload: DashboardLayoutUpdateRequest,
|
|
view_id: str | None = Query(default=None, max_length=255),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(get_api_principal),
|
|
) -> DashboardLayoutResponse:
|
|
_require_read(principal)
|
|
try:
|
|
layout = save_dashboard_layout(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
account_id=principal.account_id,
|
|
view_id=view_id,
|
|
expected_revision=payload.expected_revision,
|
|
layout_version=payload.layout_version,
|
|
placements=payload.placements,
|
|
known_widget_ids=payload.known_widget_ids,
|
|
)
|
|
session.commit()
|
|
except DashboardLayoutConflict as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=str(exc),
|
|
) from exc
|
|
except DashboardLayoutLimitExceeded as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
detail=str(exc),
|
|
) from exc
|
|
except IntegrityError as exc:
|
|
session.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=(
|
|
"The dashboard layout changed in another session. "
|
|
"Reload it before saving."
|
|
),
|
|
) from exc
|
|
return _response(layout, view_id=view_id)
|
|
|
|
|
|
@router.delete("/layout", status_code=status.HTTP_204_NO_CONTENT)
|
|
def api_delete_dashboard_layout(
|
|
view_id: str | None = Query(default=None, max_length=255),
|
|
session: Session = Depends(get_session),
|
|
principal: ApiPrincipal = Depends(get_api_principal),
|
|
) -> None:
|
|
_require_read(principal)
|
|
delete_dashboard_layout(
|
|
session,
|
|
tenant_id=principal.tenant_id,
|
|
account_id=principal.account_id,
|
|
view_id=view_id,
|
|
)
|
|
session.commit()
|
|
|
|
|
|
__all__ = ["router"]
|