feat(scheduling): persist candidate slot ordering
This commit is contained in:
@@ -20,6 +20,7 @@ from govoplan_core.core.poll import (
|
||||
PollAnswerRequest,
|
||||
PollCapabilityError,
|
||||
PollCreateCommand,
|
||||
PollOptionOrderCommand,
|
||||
PollOptionUpdateCommand,
|
||||
PollOptionRequest,
|
||||
PollResponseRef,
|
||||
@@ -207,7 +208,10 @@ def _poll_status_for_request(status: str) -> str:
|
||||
|
||||
|
||||
def _active_slots(request: SchedulingRequest) -> list[SchedulingCandidateSlot]:
|
||||
return [slot for slot in request.slots if slot.deleted_at is None]
|
||||
return sorted(
|
||||
(slot for slot in request.slots if slot.deleted_at is None),
|
||||
key=lambda slot: (slot.position, slot.id),
|
||||
)
|
||||
|
||||
|
||||
def _active_participants(request: SchedulingRequest) -> list[SchedulingParticipant]:
|
||||
@@ -2397,35 +2401,6 @@ def _candidate_slot_changes_from_reconcile(
|
||||
)
|
||||
|
||||
|
||||
def _validate_slot_reconciliation_order(
|
||||
current_slots: list[SchedulingCandidateSlot],
|
||||
supplied_slots: list[SchedulingCandidateSlotReconcileInput],
|
||||
) -> None:
|
||||
supplied_existing_ids = [item.id for item in supplied_slots if item.id is not None]
|
||||
retained_ids = {
|
||||
item.id
|
||||
for item in supplied_slots
|
||||
if item.id is not None
|
||||
}
|
||||
expected_existing_ids = [
|
||||
slot.id
|
||||
for slot in current_slots
|
||||
if slot.id in retained_ids
|
||||
]
|
||||
if supplied_existing_ids != expected_existing_ids:
|
||||
raise SchedulingError(
|
||||
"Existing candidate slots cannot be reordered; retain their order and append new slots"
|
||||
)
|
||||
encountered_new = False
|
||||
for item in supplied_slots:
|
||||
if item.id is None:
|
||||
encountered_new = True
|
||||
elif encountered_new:
|
||||
raise SchedulingError(
|
||||
"New candidate slots must be appended after existing slots"
|
||||
)
|
||||
|
||||
|
||||
def _reconcile_scheduling_slots(
|
||||
session: Session,
|
||||
*,
|
||||
@@ -2449,7 +2424,6 @@ def _reconcile_scheduling_slots(
|
||||
)
|
||||
if unknown_ids:
|
||||
raise SchedulingError("Unknown scheduling candidate slot")
|
||||
_validate_slot_reconciliation_order(current_slots, supplied_slots)
|
||||
|
||||
supplied_ids = {
|
||||
item.id
|
||||
@@ -2514,6 +2488,7 @@ def _reconcile_scheduling_slots(
|
||||
)
|
||||
|
||||
participation_provider = _poll_participation_provider()
|
||||
created_slots: list[SchedulingCandidateSlot] = []
|
||||
for item in new_inputs:
|
||||
changes = _candidate_slot_changes_from_reconcile(request, item)
|
||||
slot = SchedulingCandidateSlot(
|
||||
@@ -2557,6 +2532,7 @@ def _reconcile_scheduling_slots(
|
||||
raise SchedulingError(str(exc)) from exc
|
||||
slot.poll_option_id = created.id
|
||||
slot.position = created.position
|
||||
created_slots.append(slot)
|
||||
|
||||
for slot in removed_slots:
|
||||
if participation_provider is None: # Guarded above; keeps type narrowing explicit.
|
||||
@@ -2574,7 +2550,40 @@ def _reconcile_scheduling_slots(
|
||||
raise SchedulingError(str(exc)) from exc
|
||||
slot.deleted_at = _now()
|
||||
|
||||
return bool(planned_updates or new_inputs or removed_slots)
|
||||
created_iter = iter(created_slots)
|
||||
ordered_slots = [
|
||||
current_by_id[item.id] if item.id is not None else next(created_iter)
|
||||
for item in supplied_slots
|
||||
]
|
||||
if any(slot.poll_option_id is None for slot in ordered_slots):
|
||||
raise SchedulingError("Scheduling slot has no backing poll option")
|
||||
position_changed = any(
|
||||
slot.position != position
|
||||
for position, slot in enumerate(ordered_slots)
|
||||
)
|
||||
try:
|
||||
_poll_provider().reorder_options(
|
||||
session,
|
||||
tenant_id=request.tenant_id,
|
||||
poll_id=request.poll_id,
|
||||
command=PollOptionOrderCommand(
|
||||
option_ids=tuple(
|
||||
cast(str, slot.poll_option_id)
|
||||
for slot in ordered_slots
|
||||
),
|
||||
),
|
||||
)
|
||||
except PollCapabilityError as exc:
|
||||
raise SchedulingError(str(exc)) from exc
|
||||
for position, slot in enumerate(ordered_slots):
|
||||
slot.position = position
|
||||
|
||||
return bool(
|
||||
planned_updates
|
||||
or new_inputs
|
||||
or removed_slots
|
||||
or position_changed
|
||||
)
|
||||
|
||||
|
||||
def _normalized_participant_identity(value: str | None) -> str | None:
|
||||
|
||||
Reference in New Issue
Block a user