feat: enhance task management with due date handling and mention support
This commit is contained in:
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import DateTime, case
|
||||
from sqlalchemy import cast as sql_cast
|
||||
from sqlalchemy import func
|
||||
@@ -18,6 +18,7 @@ from app.core.time import utcnow
|
||||
from app.db.session import get_session
|
||||
from app.models.activity_events import ActivityEvent
|
||||
from app.models.agents import Agent
|
||||
from app.models.boards import Board
|
||||
from app.models.tasks import Task
|
||||
from app.schemas.metrics import (
|
||||
DashboardBucketKey,
|
||||
@@ -38,6 +39,8 @@ router = APIRouter(prefix="/metrics", tags=["metrics"])
|
||||
ERROR_EVENT_PATTERN = "%failed"
|
||||
_RUNTIME_TYPE_REFERENCES = (UUID, AsyncSession)
|
||||
RANGE_QUERY = Query(default="24h")
|
||||
BOARD_ID_QUERY = Query(default=None)
|
||||
GROUP_ID_QUERY = Query(default=None)
|
||||
SESSION_DEP = Depends(get_session)
|
||||
ORG_MEMBER_DEP = Depends(require_org_member)
|
||||
|
||||
@@ -385,16 +388,54 @@ async def _tasks_in_progress(
|
||||
return int(result)
|
||||
|
||||
|
||||
async def _resolve_dashboard_board_ids(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
ctx: OrganizationContext,
|
||||
board_id: UUID | None,
|
||||
group_id: UUID | None,
|
||||
) -> list[UUID]:
|
||||
board_ids = await list_accessible_board_ids(session, member=ctx.member, write=False)
|
||||
if not board_ids:
|
||||
return []
|
||||
allowed = set(board_ids)
|
||||
|
||||
if board_id is not None and board_id not in allowed:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if group_id is None:
|
||||
return [board_id] if board_id is not None else board_ids
|
||||
|
||||
group_board_ids = list(
|
||||
await session.exec(
|
||||
select(Board.id)
|
||||
.where(col(Board.organization_id) == ctx.member.organization_id)
|
||||
.where(col(Board.board_group_id) == group_id)
|
||||
.where(col(Board.id).in_(board_ids)),
|
||||
),
|
||||
)
|
||||
if board_id is not None:
|
||||
return [board_id] if board_id in set(group_board_ids) else []
|
||||
return group_board_ids
|
||||
|
||||
|
||||
@router.get("/dashboard", response_model=DashboardMetrics)
|
||||
async def dashboard_metrics(
|
||||
range_key: DashboardRangeKey = RANGE_QUERY,
|
||||
board_id: UUID | None = BOARD_ID_QUERY,
|
||||
group_id: UUID | None = GROUP_ID_QUERY,
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
||||
) -> DashboardMetrics:
|
||||
"""Return dashboard KPIs and time-series data for accessible boards."""
|
||||
primary = _resolve_range(range_key)
|
||||
comparison = _comparison_range(primary)
|
||||
board_ids = await list_accessible_board_ids(session, member=ctx.member, write=False)
|
||||
board_ids = await _resolve_dashboard_board_ids(
|
||||
session,
|
||||
ctx=ctx,
|
||||
board_id=board_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
throughput_primary = await _query_throughput(session, primary, board_ids)
|
||||
throughput_comparison = await _query_throughput(session, comparison, board_ids)
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.core.error_handling import (
|
||||
_error_payload,
|
||||
_get_request_id,
|
||||
_http_exception_exception_handler,
|
||||
_json_safe,
|
||||
_request_validation_exception_handler,
|
||||
_response_validation_exception_handler,
|
||||
install_error_handling,
|
||||
@@ -209,6 +210,20 @@ def test_error_payload_omits_request_id_when_none() -> None:
|
||||
assert _error_payload(detail="x", request_id=None) == {"detail": "x"}
|
||||
|
||||
|
||||
def test_json_safe_handles_binary_inputs() -> None:
|
||||
assert _json_safe(b"\xf0\x9f\x92\xa1") == "💡"
|
||||
assert _json_safe(bytearray(b"hello")) == "hello"
|
||||
assert _json_safe(memoryview(b"world")) == "world"
|
||||
|
||||
|
||||
def test_json_safe_falls_back_to_string_for_unknown_objects() -> None:
|
||||
class Weird:
|
||||
def __str__(self) -> str:
|
||||
return "weird-value"
|
||||
|
||||
assert _json_safe(Weird()) == "weird-value"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_validation_exception_wrapper_rejects_wrong_exception() -> None:
|
||||
req = Request({"type": "http", "headers": [], "state": {}})
|
||||
|
||||
128
backend/tests/test_metrics_filters.py
Normal file
128
backend/tests/test_metrics_filters.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.api import metrics as metrics_api
|
||||
|
||||
|
||||
class _FakeSession:
|
||||
def __init__(self, exec_result: list[object]) -> None:
|
||||
self._exec_result = exec_result
|
||||
|
||||
async def exec(self, _statement: object) -> list[object]:
|
||||
return self._exec_result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_dashboard_board_ids_returns_requested_board(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
board_id = uuid4()
|
||||
|
||||
async def _accessible(*_args: object, **_kwargs: object) -> list[object]:
|
||||
return [board_id]
|
||||
|
||||
monkeypatch.setattr(
|
||||
metrics_api,
|
||||
"list_accessible_board_ids",
|
||||
_accessible,
|
||||
)
|
||||
ctx = SimpleNamespace(member=SimpleNamespace(organization_id=uuid4()))
|
||||
|
||||
resolved = await metrics_api._resolve_dashboard_board_ids(
|
||||
_FakeSession([]),
|
||||
ctx=ctx,
|
||||
board_id=board_id,
|
||||
group_id=None,
|
||||
)
|
||||
|
||||
assert resolved == [board_id]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_dashboard_board_ids_rejects_inaccessible_board(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
accessible_board_id = uuid4()
|
||||
requested_board_id = uuid4()
|
||||
|
||||
async def _accessible(*_args: object, **_kwargs: object) -> list[object]:
|
||||
return [accessible_board_id]
|
||||
|
||||
monkeypatch.setattr(
|
||||
metrics_api,
|
||||
"list_accessible_board_ids",
|
||||
_accessible,
|
||||
)
|
||||
ctx = SimpleNamespace(member=SimpleNamespace(organization_id=uuid4()))
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await metrics_api._resolve_dashboard_board_ids(
|
||||
_FakeSession([]),
|
||||
ctx=ctx,
|
||||
board_id=requested_board_id,
|
||||
group_id=None,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_dashboard_board_ids_filters_by_group(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
board_a = uuid4()
|
||||
board_b = uuid4()
|
||||
group_id = uuid4()
|
||||
|
||||
async def _accessible(*_args: object, **_kwargs: object) -> list[object]:
|
||||
return [board_a, board_b]
|
||||
|
||||
monkeypatch.setattr(
|
||||
metrics_api,
|
||||
"list_accessible_board_ids",
|
||||
_accessible,
|
||||
)
|
||||
ctx = SimpleNamespace(member=SimpleNamespace(organization_id=uuid4()))
|
||||
session = _FakeSession([board_b])
|
||||
|
||||
resolved = await metrics_api._resolve_dashboard_board_ids(
|
||||
session,
|
||||
ctx=ctx,
|
||||
board_id=None,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
assert resolved == [board_b]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_dashboard_board_ids_returns_empty_when_board_not_in_group(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
board_id = uuid4()
|
||||
group_id = uuid4()
|
||||
|
||||
async def _accessible(*_args: object, **_kwargs: object) -> list[object]:
|
||||
return [board_id]
|
||||
|
||||
monkeypatch.setattr(
|
||||
metrics_api,
|
||||
"list_accessible_board_ids",
|
||||
_accessible,
|
||||
)
|
||||
ctx = SimpleNamespace(member=SimpleNamespace(organization_id=uuid4()))
|
||||
session = _FakeSession([])
|
||||
|
||||
resolved = await metrics_api._resolve_dashboard_board_ids(
|
||||
session,
|
||||
ctx=ctx,
|
||||
board_id=board_id,
|
||||
group_id=group_id,
|
||||
)
|
||||
|
||||
assert resolved == []
|
||||
Reference in New Issue
Block a user