feat: repeat tasks after completion
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 3m31s

This commit is contained in:
2026-09-10 21:21:28 +08:00
parent 84592db098
commit 64b8525720
14 changed files with 849 additions and 114 deletions
+41
View File
@@ -1,5 +1,6 @@
from datetime import datetime
from uuid import UUID
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
@@ -29,6 +30,20 @@ class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
username: str
timezone: str
class UserUpdate(BaseModel):
timezone: str = Field(min_length=1, max_length=64)
@field_validator("timezone")
@classmethod
def validate_timezone(cls, value: str) -> str:
try:
ZoneInfo(value)
except (ZoneInfoNotFoundError, ValueError) as exc:
raise ValueError("timezone must be a valid IANA timezone") from exc
return value
class SessionOut(BaseModel):
@@ -108,6 +123,8 @@ class TaskCreate(BaseModel):
due_has_time: bool = False
parent_id: UUID | None = None
rrule: str | None = Field(default=None, min_length=5, max_length=1000)
trigger_mode: str | None = Field(default=None, pattern="^(scheduled|after_completion)$")
after_completion_days: int | None = Field(default=None, ge=1, le=3650)
@field_validator("title")
@classmethod
@@ -117,6 +134,22 @@ class TaskCreate(BaseModel):
raise ValueError("title cannot be blank")
return value
@model_validator(mode="after")
def validate_recurrence(self):
has_recurrence = self.rrule is not None or self.trigger_mode is not None or self.after_completion_days is not None
if has_recurrence and self.due_at is None:
raise ValueError("recurrence requires due_at")
if has_recurrence and self.parent_id is not None:
raise ValueError("only top-level tasks can recur")
if self.trigger_mode == "after_completion":
if self.after_completion_days is None or self.rrule is not None:
raise ValueError("after_completion requires days and no rrule")
elif self.trigger_mode == "scheduled" and self.rrule is None:
raise ValueError("scheduled recurrence requires rrule")
elif self.trigger_mode is None and self.after_completion_days is not None:
raise ValueError("after_completion_days requires after_completion mode")
return self
class TaskUpdate(BaseModel):
title: str | None = Field(default=None, min_length=1, max_length=500)
@@ -188,6 +221,7 @@ class BatchTaskUpdate(BaseModel):
list_id: UUID | None = None
due_at: datetime | None = None
soft_delete: bool | None = None
versions: dict[UUID, int] | None = None
@model_validator(mode="after")
def require_operation(self):
@@ -196,6 +230,13 @@ class BatchTaskUpdate(BaseModel):
raise ValueError("at least one batch operation is required")
if self.soft_delete is False:
raise ValueError("soft_delete can only be true")
if self.completed is True and self.versions is None:
raise ValueError("completion requires versions")
if self.versions is not None:
if set(self.versions) != set(self.task_ids):
raise ValueError("versions must cover every task")
if any(version < 1 for version in self.versions.values()):
raise ValueError("versions must be positive")
return self