Add initial polling backend logic

This commit is contained in:
2026-07-12 16:26:45 +02:00
parent ddf1fcc217
commit d0e68f5aa6
12 changed files with 1325 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
PollKind = Literal["single_choice", "multiple_choice", "yes_no", "ranked_choice", "availability"]
PollStatus = Literal["draft", "open", "closed", "decided", "archived"]
PollVisibility = Literal["private", "tenant", "public", "unlisted"]
PollResultVisibility = Literal["organizer", "after_response", "after_close", "public"]
AvailabilityValue = Literal["available", "maybe", "unavailable"]
class PollOptionInput(BaseModel):
model_config = ConfigDict(extra="forbid")
key: str | None = Field(default=None, min_length=1, max_length=120)
label: str = Field(min_length=1, max_length=500)
description: str | None = None
value: dict[str, Any] | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
class PollCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str = Field(min_length=1, max_length=500)
slug: str | None = Field(default=None, max_length=120)
description: str | None = None
kind: PollKind = "single_choice"
status: PollStatus = "draft"
visibility: PollVisibility = "private"
result_visibility: PollResultVisibility = "after_close"
allow_anonymous: bool = False
allow_response_update: bool = True
min_choices: int = Field(default=1, ge=0)
max_choices: int | None = Field(default=None, ge=1)
opens_at: datetime | None = None
closes_at: datetime | None = None
options: list[PollOptionInput] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)
@model_validator(mode="after")
def validate_window(self) -> "PollCreateRequest":
if self.opens_at is not None and self.closes_at is not None and self.closes_at <= self.opens_at:
raise ValueError("closes_at must be after opens_at")
return self
class PollUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str | None = Field(default=None, min_length=1, max_length=500)
description: str | None = None
visibility: PollVisibility | None = None
result_visibility: PollResultVisibility | None = None
allow_anonymous: bool | None = None
allow_response_update: bool | None = None
min_choices: int | None = Field(default=None, ge=0)
max_choices: int | None = Field(default=None, ge=1)
opens_at: datetime | None = None
closes_at: datetime | None = None
metadata: dict[str, Any] | None = None
class PollOptionResponse(BaseModel):
id: str
key: str
label: str
description: str | None = None
position: int
value: dict[str, Any] | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
class PollResponseItem(BaseModel):
id: str
respondent_id: str | None = None
respondent_label: str | None = None
answers: list[dict[str, Any]] = Field(default_factory=list)
submitted_at: datetime
created_at: datetime
updated_at: datetime
metadata: dict[str, Any] = Field(default_factory=dict)
class PollResponse(BaseModel):
id: str
tenant_id: str
slug: str
title: str
description: str | None = None
kind: str
status: str
visibility: str
result_visibility: str
allow_anonymous: bool
allow_response_update: bool
min_choices: int
max_choices: int | None = None
opens_at: datetime | None = None
closes_at: datetime | None = None
closed_at: datetime | None = None
decided_at: datetime | None = None
decided_option_id: str | None = None
created_by_user_id: str | None = None
created_at: datetime
updated_at: datetime
metadata: dict[str, Any] = Field(default_factory=dict)
options: list[PollOptionResponse] = Field(default_factory=list)
class PollListResponse(BaseModel):
polls: list[PollResponse] = Field(default_factory=list)
class PollAnswerInput(BaseModel):
model_config = ConfigDict(extra="forbid")
option_id: str | None = None
option_key: str | None = None
value: Any = None
rank: int | None = Field(default=None, ge=1)
class PollSubmitResponseRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
respondent_id: str | None = Field(default=None, max_length=255)
respondent_label: str | None = Field(default=None, max_length=500)
answers: list[PollAnswerInput] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)
class PollOptionResultResponse(BaseModel):
option_id: str
option_key: str
label: str
count: int = 0
score: int = 0
values: dict[str, int] = Field(default_factory=dict)
ranks: dict[int, int] = Field(default_factory=dict)
class PollResultSummaryResponse(BaseModel):
poll_id: str
kind: str
status: str
response_count: int
option_results: list[PollOptionResultResponse] = Field(default_factory=list)
leading_option_ids: list[str] = Field(default_factory=list)
class PollDecisionRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
option_id: str | None = None
option_key: str | None = None
class PollStatusResponse(BaseModel):
poll: PollResponse
class PollResponseListResponse(BaseModel):
responses: list[PollResponseItem] = Field(default_factory=list)