refactor(scheduling): simplify candidate slot updates
This commit is contained in:
@@ -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,
|
||||||
poll_id=request.poll_id,
|
request=request,
|
||||||
option_id=slot.poll_option_id,
|
slot=slot,
|
||||||
command=PollOptionUpdateCommand(
|
changes=changes,
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user