fix(responses): reject stale scheduling answers
This commit is contained in:
@@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
|
||||
SchedulingStatus = Literal["draft", "collecting", "closed", "decided", "handed_off", "cancelled", "archived"]
|
||||
@@ -13,6 +14,16 @@ SchedulingResultVisibility = Literal["organizer", "after_response", "after_close
|
||||
SchedulingAvailabilityValue = Literal["available", "maybe", "unavailable"]
|
||||
|
||||
|
||||
def _known_timezone(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
ZoneInfo(value)
|
||||
except ZoneInfoNotFoundError as exc:
|
||||
raise ValueError("timezone must be a valid IANA timezone") from exc
|
||||
return value
|
||||
|
||||
|
||||
class SchedulingCalendarPreferences(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@@ -28,12 +39,14 @@ class SchedulingCandidateSlotInput(BaseModel):
|
||||
|
||||
label: str | None = Field(default=None, max_length=500)
|
||||
description: str | None = None
|
||||
start_at: datetime
|
||||
end_at: datetime
|
||||
start_at: AwareDatetime
|
||||
end_at: AwareDatetime
|
||||
timezone: str | None = Field(default=None, max_length=100)
|
||||
location: str | None = Field(default=None, max_length=500)
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
_validate_timezone = field_validator("timezone")(_known_timezone)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_range(self) -> "SchedulingCandidateSlotInput":
|
||||
if self.end_at <= self.start_at:
|
||||
@@ -52,12 +65,14 @@ class SchedulingCandidateSlotUpdateRequest(BaseModel):
|
||||
|
||||
label: str | None = Field(default=None, min_length=1, max_length=500)
|
||||
description: str | None = None
|
||||
start_at: datetime | None = None
|
||||
end_at: datetime | None = None
|
||||
start_at: AwareDatetime | None = None
|
||||
end_at: AwareDatetime | None = None
|
||||
timezone: str | None = Field(default=None, min_length=1, max_length=100)
|
||||
location: str | None = Field(default=None, max_length=500)
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
_validate_timezone = field_validator("timezone")(_known_timezone)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_range(self) -> "SchedulingCandidateSlotUpdateRequest":
|
||||
if self.start_at is not None and self.end_at is not None and self.end_at <= self.start_at:
|
||||
@@ -94,6 +109,8 @@ class SchedulingRequestCreateRequest(BaseModel):
|
||||
create_participant_invitations: bool = True
|
||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
_validate_timezone = field_validator("timezone")(_known_timezone)
|
||||
|
||||
|
||||
class SchedulingRequestUpdateRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
@@ -119,6 +136,7 @@ class SchedulingCandidateSlotResponse(BaseModel):
|
||||
timezone: str
|
||||
location: str | None = None
|
||||
position: int
|
||||
revision: str
|
||||
freebusy_checked_at: datetime | None = None
|
||||
freebusy_status: str | None = None
|
||||
freebusy_conflicts: list[dict[str, Any]] = Field(default_factory=list)
|
||||
@@ -192,6 +210,7 @@ class SchedulingAvailabilityAnswerInput(BaseModel):
|
||||
|
||||
slot_id: str
|
||||
value: SchedulingAvailabilityValue
|
||||
option_revision: str = Field(min_length=64, max_length=64, pattern=r"^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
class SchedulingAvailabilityResponseRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user