refactor(scheduling): simplify candidate slot updates

This commit is contained in:
2026-07-21 12:59:25 +02:00
parent 770ecf8786
commit bb7cea03df

View File

@@ -2,8 +2,9 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any from typing import Any, cast
from sqlalchemy.orm import Session, object_session from sqlalchemy.orm import Session, object_session
@@ -1416,6 +1417,121 @@ def update_scheduling_request(
return request return request
@dataclass(frozen=True, slots=True)
class _CandidateSlotChanges:
label: str
description: str | None
start_at: datetime
end_at: datetime
timezone: str
location: str | None
metadata: dict[str, Any]
normalized_start: datetime
normalized_end: datetime
def _candidate_slot_changes(
slot: SchedulingCandidateSlot,
payload: SchedulingCandidateSlotUpdateRequest,
) -> _CandidateSlotChanges:
supplied = payload.model_fields_set
for field in ("label", "start_at", "end_at", "timezone"):
if field in supplied and getattr(payload, field) is None:
raise SchedulingError(f"{field} cannot be null")
label = cast(str, payload.label if "label" in supplied else slot.label)
description = payload.description if "description" in supplied else slot.description
start_at = cast(datetime, payload.start_at if "start_at" in supplied else slot.start_at)
end_at = cast(datetime, payload.end_at if "end_at" in supplied else slot.end_at)
timezone_name = cast(str, payload.timezone if "timezone" in supplied else slot.timezone)
location = payload.location if "location" in supplied else slot.location
metadata = (payload.metadata or {}) if "metadata" in supplied else (slot.metadata_ or {})
normalized_start = cast(datetime, response_datetime(start_at))
normalized_end = cast(datetime, response_datetime(end_at))
if normalized_end <= normalized_start:
raise SchedulingError("end_at must be after start_at")
return _CandidateSlotChanges(
label=label,
description=description,
start_at=start_at,
end_at=end_at,
timezone=timezone_name,
location=location,
metadata=metadata,
normalized_start=normalized_start,
normalized_end=normalized_end,
)
def _candidate_slot_change_flags(
slot: SchedulingCandidateSlot,
changes: _CandidateSlotChanges,
) -> tuple[bool, bool]:
temporal_or_location_changed = (
changes.normalized_start != response_datetime(slot.start_at)
or changes.normalized_end != response_datetime(slot.end_at)
or changes.timezone != slot.timezone
or changes.location != slot.location
)
changed = (
changes.label != slot.label
or changes.description != slot.description
or temporal_or_location_changed
or changes.metadata != (slot.metadata_ or {})
)
return changed, temporal_or_location_changed
def _update_candidate_poll_option(
session: Session,
*,
tenant_id: str,
request: SchedulingRequest,
slot: SchedulingCandidateSlot,
changes: _CandidateSlotChanges,
) -> None:
try:
_poll_provider().update_option(
session,
tenant_id=tenant_id,
poll_id=cast(str, request.poll_id),
option_id=cast(str, slot.poll_option_id),
command=PollOptionUpdateCommand(
label=changes.label,
description=changes.description,
value={
"slot_id": slot.id,
"start_at": changes.normalized_start.isoformat(),
"end_at": changes.normalized_end.isoformat(),
"timezone": changes.timezone,
"location": changes.location,
},
metadata=changes.metadata,
),
)
except PollCapabilityError as exc:
raise SchedulingError(str(exc)) from exc
def _apply_candidate_slot_changes(
slot: SchedulingCandidateSlot,
changes: _CandidateSlotChanges,
*,
temporal_or_location_changed: bool,
) -> None:
slot.label = changes.label
slot.description = changes.description
slot.start_at = changes.start_at
slot.end_at = changes.end_at
slot.timezone = changes.timezone
slot.location = changes.location
slot.metadata_ = changes.metadata
if temporal_or_location_changed:
slot.freebusy_checked_at = None
slot.freebusy_status = None
slot.freebusy_conflicts = []
def update_scheduling_candidate_slot( def update_scheduling_candidate_slot(
session: Session, session: Session,
*, *,
@@ -1440,35 +1556,8 @@ def update_scheduling_candidate_slot(
if slot.poll_option_id is None: if slot.poll_option_id is None:
raise SchedulingError("Scheduling slot has no backing poll option") raise SchedulingError("Scheduling slot has no backing poll option")
supplied = payload.model_fields_set changes = _candidate_slot_changes(slot, payload)
for field in ("label", "start_at", "end_at", "timezone"): changed, temporal_or_location_changed = _candidate_slot_change_flags(slot, changes)
if field in supplied and getattr(payload, field) is None:
raise SchedulingError(f"{field} cannot be null")
label = payload.label if "label" in supplied else slot.label
description = payload.description if "description" in supplied else slot.description
start_at = payload.start_at if "start_at" in supplied else slot.start_at
end_at = payload.end_at if "end_at" in supplied else slot.end_at
timezone_name = payload.timezone if "timezone" in supplied else slot.timezone
location = payload.location if "location" in supplied else slot.location
metadata = (payload.metadata or {}) if "metadata" in supplied else (slot.metadata_ or {})
normalized_start = response_datetime(start_at)
normalized_end = response_datetime(end_at)
if normalized_end <= normalized_start:
raise SchedulingError("end_at must be after start_at")
temporal_or_location_changed = (
normalized_start != response_datetime(slot.start_at)
or normalized_end != response_datetime(slot.end_at)
or timezone_name != slot.timezone
or location != slot.location
)
changed = (
label != slot.label
or description != slot.description
or temporal_or_location_changed
or metadata != (slot.metadata_ or {})
)
if not changed: if not changed:
return request return request
if temporal_or_location_changed and slot.tentative_hold_event_id: if temporal_or_location_changed and slot.tentative_hold_event_id:
@@ -1476,27 +1565,13 @@ def update_scheduling_candidate_slot(
"A slot with a tentative calendar hold cannot change time, timezone, or location" "A slot with a tentative calendar hold cannot change time, timezone, or location"
) )
try: _update_candidate_poll_option(
_poll_provider().update_option( session,
session, tenant_id=tenant_id,
tenant_id=tenant_id, request=request,
poll_id=request.poll_id, slot=slot,
option_id=slot.poll_option_id, changes=changes,
command=PollOptionUpdateCommand( )
label=label,
description=description,
value={
"slot_id": slot.id,
"start_at": normalized_start.isoformat(),
"end_at": normalized_end.isoformat(),
"timezone": timezone_name,
"location": location,
},
metadata=metadata,
),
)
except PollCapabilityError as exc:
raise SchedulingError(str(exc)) from exc
refresh_participant_response_state( refresh_participant_response_state(
session, session,
@@ -1504,17 +1579,11 @@ def update_scheduling_candidate_slot(
reset_missing=True, reset_missing=True,
) )
slot.label = label _apply_candidate_slot_changes(
slot.description = description slot,
slot.start_at = start_at changes,
slot.end_at = end_at temporal_or_location_changed=temporal_or_location_changed,
slot.timezone = timezone_name )
slot.location = location
slot.metadata_ = metadata
if temporal_or_location_changed:
slot.freebusy_checked_at = None
slot.freebusy_status = None
slot.freebusy_conflicts = []
session.flush() session.flush()
return request return request