Add poll-backed scheduling backend

This commit is contained in:
2026-07-12 18:12:20 +02:00
parent cfea0edb97
commit ace32a2a3d
12 changed files with 1378 additions and 7 deletions

View File

@@ -0,0 +1,165 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
from govoplan_poll.backend.schemas import PollResultSummaryResponse
SchedulingStatus = Literal["draft", "collecting", "closed", "decided", "handed_off", "cancelled", "archived"]
SchedulingParticipantType = Literal["internal", "external", "resource"]
SchedulingParticipantStatus = Literal["draft", "invited", "responded", "declined", "removed"]
SchedulingResultVisibility = Literal["organizer", "after_response", "after_close", "public"]
class SchedulingCalendarPreferences(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled: bool = False
calendar_id: str | None = Field(default=None, max_length=36)
freebusy_enabled: bool = False
tentative_holds_enabled: bool = False
create_event_on_decision: bool = False
class SchedulingCandidateSlotInput(BaseModel):
model_config = ConfigDict(extra="forbid")
label: str | None = Field(default=None, max_length=500)
description: str | None = None
start_at: datetime
end_at: datetime
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)
@model_validator(mode="after")
def validate_range(self) -> "SchedulingCandidateSlotInput":
if self.end_at <= self.start_at:
raise ValueError("end_at must be after start_at")
return self
class SchedulingParticipantInput(BaseModel):
model_config = ConfigDict(extra="forbid")
respondent_id: str | None = Field(default=None, max_length=255)
display_name: str | None = Field(default=None, max_length=500)
email: str | None = Field(default=None, max_length=320)
participant_type: SchedulingParticipantType = "external"
required: bool = True
metadata: dict[str, Any] = Field(default_factory=dict)
class SchedulingRequestCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str = Field(min_length=1, max_length=500)
description: str | None = None
location: str | None = Field(default=None, max_length=500)
timezone: str = Field(default="UTC", max_length=100)
status: Literal["draft", "collecting"] = "draft"
deadline_at: datetime | None = None
allow_external_participants: bool = True
allow_participant_updates: bool = True
result_visibility: SchedulingResultVisibility = "after_close"
calendar: SchedulingCalendarPreferences = Field(default_factory=SchedulingCalendarPreferences)
slots: list[SchedulingCandidateSlotInput] = Field(default_factory=list, min_length=1)
participants: list[SchedulingParticipantInput] = Field(default_factory=list)
create_participant_invitations: bool = True
metadata: dict[str, Any] = Field(default_factory=dict)
class SchedulingRequestUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str | None = Field(default=None, min_length=1, max_length=500)
description: str | None = None
location: str | None = Field(default=None, max_length=500)
deadline_at: datetime | None = None
allow_external_participants: bool | None = None
allow_participant_updates: bool | None = None
result_visibility: SchedulingResultVisibility | None = None
calendar: SchedulingCalendarPreferences | None = None
metadata: dict[str, Any] | None = None
class SchedulingCandidateSlotResponse(BaseModel):
id: str
poll_option_id: str | None = None
label: str
description: str | None = None
start_at: datetime
end_at: datetime
timezone: str
location: str | None = None
position: int
metadata: dict[str, Any] = Field(default_factory=dict)
class SchedulingParticipantResponse(BaseModel):
id: str
respondent_id: str | None = None
display_name: str | None = None
email: str | None = None
participant_type: str
required: bool
status: str
poll_invitation_id: str | None = None
invitation_token: str | None = None
last_invited_at: datetime | None = None
responded_at: datetime | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
class SchedulingRequestResponse(BaseModel):
id: str
tenant_id: str
title: str
description: str | None = None
location: str | None = None
timezone: str
status: str
poll_id: str | None = None
selected_slot_id: str | None = None
organizer_user_id: str | None = None
deadline_at: datetime | None = None
allow_external_participants: bool
allow_participant_updates: bool
result_visibility: str
calendar_integration_enabled: bool
calendar_id: str | None = None
calendar_freebusy_enabled: bool
calendar_hold_enabled: bool
create_calendar_event_on_decision: bool
calendar_event_id: str | None = None
handed_off_at: datetime | None = None
cancelled_at: datetime | None = None
created_at: datetime
updated_at: datetime
metadata: dict[str, Any] = Field(default_factory=dict)
slots: list[SchedulingCandidateSlotResponse] = Field(default_factory=list)
participants: list[SchedulingParticipantResponse] = Field(default_factory=list)
class SchedulingRequestListResponse(BaseModel):
requests: list[SchedulingRequestResponse] = Field(default_factory=list)
class SchedulingStatusResponse(BaseModel):
request: SchedulingRequestResponse
class SchedulingDecisionRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
slot_id: str | None = None
poll_option_id: str | None = None
handoff_to_calendar: bool | None = None
class SchedulingSummaryResponse(BaseModel):
request: SchedulingRequestResponse
poll_summary: PollResultSummaryResponse