feat(scheduling): use central people picker
This commit is contained in:
@@ -20,6 +20,10 @@ from govoplan_core.core.modules import (
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.core.poll import CAPABILITY_POLL_SCHEDULING
|
||||
from govoplan_core.core.people import (
|
||||
CAPABILITY_ACCESS_PEOPLE_SEARCH,
|
||||
CAPABILITY_ADDRESSES_PEOPLE_SEARCH,
|
||||
)
|
||||
from govoplan_core.core.policy import CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY
|
||||
from govoplan_poll.backend.participation import CAPABILITY_POLL_PARTICIPATION_GATEWAY
|
||||
from govoplan_scheduling.backend.db import models as scheduling_models # noqa: F401 - populate Scheduling ORM metadata
|
||||
@@ -138,6 +142,8 @@ manifest = ModuleManifest(
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
CAPABILITY_CALENDAR_SCHEDULING,
|
||||
CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY,
|
||||
CAPABILITY_ACCESS_PEOPLE_SEARCH,
|
||||
CAPABILITY_ADDRESSES_PEOPLE_SEARCH,
|
||||
),
|
||||
required_capabilities=(
|
||||
CAPABILITY_POLL_SCHEDULING,
|
||||
@@ -154,7 +160,8 @@ manifest = ModuleManifest(
|
||||
ModuleInterfaceRequirement(name="poll.governed_participation", version_min="0.1.10", version_max_exclusive="0.2.0"),
|
||||
ModuleInterfaceRequirement(name="evaluation.feedback", version_min="0.1.8", version_max_exclusive="0.2.0", optional=True),
|
||||
ModuleInterfaceRequirement(name="notifications.dispatch", version_min="0.1.8", version_max_exclusive="0.2.0", optional=True),
|
||||
ModuleInterfaceRequirement(name="addresses.lookup", version_min="0.1.0", version_max_exclusive="0.2.0", optional=True),
|
||||
ModuleInterfaceRequirement(name=CAPABILITY_ACCESS_PEOPLE_SEARCH, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True),
|
||||
ModuleInterfaceRequirement(name=CAPABILITY_ADDRESSES_PEOPLE_SEARCH, version_min="0.1.0", version_max_exclusive="0.2.0", optional=True),
|
||||
ModuleInterfaceRequirement(name="calendar.scheduling", version_min="0.1.8", version_max_exclusive="0.2.0", optional=True),
|
||||
),
|
||||
permissions=PERMISSIONS,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
||||
@@ -9,11 +8,10 @@ from sqlalchemy.orm import Session
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.core.calendar import CALENDAR_AVAILABILITY_READ_SCOPE, CALENDAR_EVENT_WRITE_SCOPE
|
||||
from govoplan_core.core.people import search_visible_people
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_scheduling.backend.manifest import ADMIN_SCOPE, READ_SCOPE, RESPOND_SCOPE, WRITE_SCOPE
|
||||
from govoplan_scheduling.backend.schemas import (
|
||||
SchedulingAddressLookupCandidate,
|
||||
SchedulingAddressLookupResponse,
|
||||
SchedulingAvailabilityResponse,
|
||||
SchedulingAvailabilityResponseRequest,
|
||||
SchedulingCalendarActionResponse,
|
||||
@@ -24,6 +22,9 @@ from govoplan_scheduling.backend.schemas import (
|
||||
SchedulingNotificationCreateRequest,
|
||||
SchedulingNotificationListResponse,
|
||||
SchedulingNotificationResponse,
|
||||
SchedulingPeopleSearchCandidate,
|
||||
SchedulingPeopleSearchGroup,
|
||||
SchedulingPeopleSearchResponse,
|
||||
SchedulingRequestCreateRequest,
|
||||
SchedulingRequestListResponse,
|
||||
SchedulingRequestResponse,
|
||||
@@ -71,39 +72,6 @@ from govoplan_scheduling.backend.service import (
|
||||
|
||||
|
||||
router = APIRouter(prefix="/scheduling", tags=["scheduling"])
|
||||
CAPABILITY_ADDRESSES_LOOKUP = "addresses.lookup"
|
||||
|
||||
|
||||
def _capability_payload(value: object) -> dict[str, Any]:
|
||||
if dataclasses.is_dataclass(value):
|
||||
return dataclasses.asdict(value)
|
||||
if isinstance(value, dict):
|
||||
return dict(value)
|
||||
payload: dict[str, Any] = {}
|
||||
for key in (
|
||||
"contact_id",
|
||||
"address_book_id",
|
||||
"display_name",
|
||||
"email",
|
||||
"email_label",
|
||||
"organization",
|
||||
"role_title",
|
||||
"tags",
|
||||
"source_kind",
|
||||
"source_ref",
|
||||
"source_revision",
|
||||
"provenance",
|
||||
):
|
||||
if hasattr(value, key):
|
||||
payload[key] = getattr(value, key)
|
||||
return payload
|
||||
|
||||
|
||||
def _registry_capability(name: str) -> object | None:
|
||||
registry = get_registry()
|
||||
if registry is None or not hasattr(registry, "has_capability") or not registry.has_capability(name):
|
||||
return None
|
||||
return registry.capability(name)
|
||||
|
||||
|
||||
def _require_scope(principal: ApiPrincipal, scope: str) -> None:
|
||||
@@ -285,21 +253,43 @@ def api_submit_public_scheduling_participation(
|
||||
return validated
|
||||
|
||||
|
||||
@router.get("/address-lookup", response_model=SchedulingAddressLookupResponse)
|
||||
def api_lookup_scheduling_addresses(
|
||||
@router.get("/people", response_model=SchedulingPeopleSearchResponse)
|
||||
def api_search_scheduling_people(
|
||||
query: str = Query(min_length=1),
|
||||
limit: int = Query(default=25, ge=1, le=100),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> SchedulingAddressLookupResponse:
|
||||
) -> SchedulingPeopleSearchResponse:
|
||||
_require_scheduling_writer(principal)
|
||||
capability = _registry_capability(CAPABILITY_ADDRESSES_LOOKUP)
|
||||
if capability is None or not hasattr(capability, "lookup"):
|
||||
return SchedulingAddressLookupResponse(available=False, candidates=[])
|
||||
candidates = getattr(capability, "lookup")(session, principal, query=query, limit=limit)
|
||||
return SchedulingAddressLookupResponse(
|
||||
available=True,
|
||||
candidates=[SchedulingAddressLookupCandidate.model_validate(_capability_payload(candidate)) for candidate in candidates],
|
||||
groups = search_visible_people(
|
||||
get_registry(),
|
||||
session,
|
||||
principal,
|
||||
query=query,
|
||||
limit=limit,
|
||||
)
|
||||
return SchedulingPeopleSearchResponse(
|
||||
groups=[
|
||||
SchedulingPeopleSearchGroup(
|
||||
key=group.key,
|
||||
label=group.label,
|
||||
candidates=[
|
||||
SchedulingPeopleSearchCandidate(
|
||||
selection_key=candidate.selection_key,
|
||||
kind=candidate.kind,
|
||||
reference_id=candidate.reference_id,
|
||||
display_name=candidate.display_name,
|
||||
email=candidate.email,
|
||||
source_module=candidate.source_module,
|
||||
source_label=candidate.source_label,
|
||||
source_revision=candidate.source_revision,
|
||||
description=candidate.description,
|
||||
)
|
||||
for candidate in group.candidates
|
||||
],
|
||||
)
|
||||
for group in groups
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -515,21 +515,25 @@ class SchedulingInvitationActionResponse(BaseModel):
|
||||
notification: SchedulingNotificationResponse | None = None
|
||||
|
||||
|
||||
class SchedulingAddressLookupCandidate(BaseModel):
|
||||
contact_id: str
|
||||
address_book_id: str
|
||||
class SchedulingPeopleSearchCandidate(BaseModel):
|
||||
"""Opaque, task-safe projection of a visible directory candidate."""
|
||||
|
||||
selection_key: str
|
||||
kind: str
|
||||
reference_id: str
|
||||
display_name: str
|
||||
email: str | None = None
|
||||
email_label: str | None = None
|
||||
organization: str | None = None
|
||||
role_title: str | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
source_kind: str = "local"
|
||||
source_ref: str | None = None
|
||||
source_module: str | None = None
|
||||
source_label: str | None = None
|
||||
source_revision: str | None = None
|
||||
provenance: dict[str, Any] = Field(default_factory=dict)
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class SchedulingAddressLookupResponse(BaseModel):
|
||||
available: bool = False
|
||||
candidates: list[SchedulingAddressLookupCandidate] = Field(default_factory=list)
|
||||
class SchedulingPeopleSearchGroup(BaseModel):
|
||||
key: str
|
||||
label: str
|
||||
candidates: list[SchedulingPeopleSearchCandidate] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SchedulingPeopleSearchResponse(BaseModel):
|
||||
groups: list[SchedulingPeopleSearchGroup] = Field(default_factory=list)
|
||||
|
||||
Reference in New Issue
Block a user