perf: batch scheduling reconciliation and add widget

This commit is contained in:
2026-07-29 14:16:29 +02:00
parent 60b01e5a16
commit b9f557e95b
7 changed files with 290 additions and 32 deletions
@@ -26,6 +26,7 @@ from govoplan_core.core.people import (
from govoplan_core.core.poll import CAPABILITY_POLL_SCHEDULING
from govoplan_core.core.poll_participation import CAPABILITY_POLL_PARTICIPATION_GATEWAY
from govoplan_core.core.policy import CAPABILITY_POLICY_SCHEDULING_PARTICIPANT_PRIVACY
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base
from govoplan_scheduling.backend.db import models as scheduling_models # noqa: F401 - populate Scheduling ORM metadata
@@ -188,6 +189,15 @@ manifest = ModuleManifest(
),
),
nav_items=(NavItem(path="/scheduling", label="Scheduling", icon="calendar-clock", required_any=(READ_SCOPE,), order=56),),
view_surfaces=(
ViewSurface(
id="scheduling.widget.open-requests",
module_id=MODULE_ID,
kind="section",
label="Scheduling requests widget",
order=45,
),
),
),
route_factory=_scheduling_router,
tenant_summary_providers=(_tenant_summary,),
+4 -20
View File
@@ -297,6 +297,7 @@ def api_search_scheduling_people(
@router.get("/requests", response_model=SchedulingRequestListResponse)
def api_list_scheduling_requests(
status_filter: str | None = Query(default=None, alias="status"),
limit: int = 100,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> SchedulingRequestListResponse:
@@ -307,21 +308,11 @@ def api_list_scheduling_requests(
actor_ids=_principal_actor_ids(principal),
can_manage=_can_manage_scheduling(principal),
status=status_filter,
limit=limit,
)
actor_ids = _principal_actor_ids(principal)
for request in requests:
refresh_participant_response_state(
session,
request=request,
actor_ids=actor_ids,
)
response = SchedulingRequestListResponse(
return SchedulingRequestListResponse(
requests=[_request_response(request, principal=principal) for request in requests]
)
# Poll responses are authoritative, while Scheduling keeps a durable
# participant projection used by its task-oriented list.
session.commit()
return response
@router.post("/requests", response_model=SchedulingRequestResponse, status_code=status.HTTP_201_CREATED)
@@ -407,16 +398,9 @@ def api_get_scheduling_request(
actor_ids=_principal_actor_ids(principal),
can_manage=_can_manage_scheduling(principal),
)
refresh_participant_response_state(
session,
request=request,
actor_ids=_principal_actor_ids(principal),
)
except SchedulingError as exc:
raise _scheduling_http_error(exc) from exc
response = _request_response(request, principal=principal)
session.commit()
return response
return _request_response(request, principal=principal)
@router.patch("/requests/{request_id}", response_model=SchedulingRequestResponse)
+74 -9
View File
@@ -7,7 +7,8 @@ from datetime import datetime, timedelta, timezone
from functools import lru_cache
from typing import Any, cast
from sqlalchemy.orm import Session, object_session
from sqlalchemy import func, or_
from sqlalchemy.orm import Session, object_session, selectinload
from govoplan_core.core.calendar import (
CalendarCapabilityError,
@@ -1303,11 +1304,34 @@ def create_scheduling_request(
return request, {}
def list_scheduling_requests(session: Session, *, tenant_id: str, status: str | None = None) -> list[SchedulingRequest]:
query = session.query(SchedulingRequest).filter(SchedulingRequest.tenant_id == tenant_id, SchedulingRequest.deleted_at.is_(None))
def list_scheduling_requests(
session: Session,
*,
tenant_id: str,
status: str | None = None,
limit: int = 100,
) -> list[SchedulingRequest]:
query = (
session.query(SchedulingRequest)
.options(
selectinload(SchedulingRequest.slots),
selectinload(SchedulingRequest.participants),
)
.filter(
SchedulingRequest.tenant_id == tenant_id,
SchedulingRequest.deleted_at.is_(None),
)
)
if status:
query = query.filter(SchedulingRequest.status == status)
return query.order_by(SchedulingRequest.created_at.desc(), SchedulingRequest.title.asc()).all()
return (
query.order_by(
SchedulingRequest.created_at.desc(),
SchedulingRequest.title.asc(),
)
.limit(max(1, min(limit, 200)))
.all()
)
def _actor_ids(values: tuple[str, ...]) -> tuple[str, ...]:
@@ -1369,12 +1393,53 @@ def list_visible_scheduling_requests(
actor_ids: tuple[str, ...],
can_manage: bool = False,
status: str | None = None,
limit: int = 100,
) -> list[SchedulingRequest]:
return [
request
for request in list_scheduling_requests(session, tenant_id=tenant_id, status=status)
if scheduling_request_is_visible(request, actor_ids=actor_ids, can_manage=can_manage)
]
query = (
session.query(SchedulingRequest)
.options(
selectinload(SchedulingRequest.slots),
selectinload(SchedulingRequest.participants),
)
.filter(
SchedulingRequest.tenant_id == tenant_id,
SchedulingRequest.deleted_at.is_(None),
)
)
if status:
query = query.filter(SchedulingRequest.status == status)
if not can_manage:
ids = _actor_ids(actor_ids)
email_ids = tuple(value.casefold() for value in ids if "@" in value)
participant_exists = (
session.query(SchedulingParticipant.id)
.filter(
SchedulingParticipant.tenant_id == tenant_id,
SchedulingParticipant.request_id == SchedulingRequest.id,
SchedulingParticipant.deleted_at.is_(None),
or_(
SchedulingParticipant.respondent_id.in_(ids or ("",)),
func.lower(SchedulingParticipant.email).in_(
email_ids or ("",)
),
),
)
.exists()
)
query = query.filter(
or_(
SchedulingRequest.organizer_user_id.in_(ids or ("",)),
participant_exists,
)
)
return (
query.order_by(
SchedulingRequest.created_at.desc(),
SchedulingRequest.title.asc(),
)
.limit(max(1, min(limit, 200)))
.all()
)
def get_scheduling_request(session: Session, *, tenant_id: str, request_id: str) -> SchedulingRequest: