T05: Calendar plugin backend — appointments + tasks + kanban + ICS + resources + recurrence — 69 tests, 86.87% coverage
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""Pydantic schemas for the Calendar plugin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
def _ensure_utc(v: datetime | None) -> datetime | None:
|
||||
"""Convert naive datetimes to UTC to prevent local-time shifts in DB storage."""
|
||||
if v is not None and isinstance(v, datetime) and v.tzinfo is None:
|
||||
return v.replace(tzinfo=UTC)
|
||||
return v
|
||||
|
||||
|
||||
class CalendarCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
color: str = Field("#3B82F6", max_length=20)
|
||||
type: str = Field("personal", pattern="^(personal|team|project|company)$")
|
||||
|
||||
|
||||
class CalendarUpdate(BaseModel):
|
||||
name: str | None = Field(None, min_length=1, max_length=200)
|
||||
color: str | None = Field(None, max_length=20)
|
||||
|
||||
|
||||
class CalendarResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
color: str
|
||||
type: str
|
||||
owner_id: str
|
||||
ics_token: str | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class ShareRequest(BaseModel):
|
||||
user_id: str | None = None
|
||||
group_id: str | None = None
|
||||
permission: str = Field(..., pattern="^(read|write)$")
|
||||
|
||||
|
||||
class ShareResponse(BaseModel):
|
||||
id: str
|
||||
calendar_id: str
|
||||
user_id: str | None = None
|
||||
group_id: str | None = None
|
||||
permission: str
|
||||
|
||||
|
||||
class EntryCreate(BaseModel):
|
||||
calendar_id: str
|
||||
entry_type: str = Field(..., pattern="^(appointment|task)$")
|
||||
subtype: str = Field("normal", pattern="^(normal|follow_up|private)$")
|
||||
title: str = Field(..., min_length=1, max_length=500)
|
||||
description: str | None = None
|
||||
start_at: datetime | None = None
|
||||
_ensure_utc_start = field_validator("start_at")(lambda cls, v: _ensure_utc(v))
|
||||
end_at: datetime | None = None
|
||||
_ensure_utc_end = field_validator("end_at")(lambda cls, v: _ensure_utc(v))
|
||||
all_day: bool = False
|
||||
location: str | None = Field(None, max_length=500)
|
||||
due_date: datetime | None = None
|
||||
_ensure_utc_due = field_validator("due_date")(lambda cls, v: _ensure_utc(v))
|
||||
priority: str = Field("medium", pattern="^(low|medium|high)$")
|
||||
status: str = Field("open", pattern="^(open|in_progress|done|cancelled)$")
|
||||
assigned_to: str | None = None
|
||||
reminder: dict[str, Any] | None = None
|
||||
recurrence: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class EntryUpdate(BaseModel):
|
||||
calendar_id: str | None = None
|
||||
subtype: str | None = Field(None, pattern="^(normal|follow_up|private)$")
|
||||
title: str | None = Field(None, min_length=1, max_length=500)
|
||||
description: str | None = None
|
||||
start_at: datetime | None = None
|
||||
_ensure_utc_start = field_validator("start_at")(lambda cls, v: _ensure_utc(v))
|
||||
end_at: datetime | None = None
|
||||
_ensure_utc_end = field_validator("end_at")(lambda cls, v: _ensure_utc(v))
|
||||
all_day: bool | None = None
|
||||
location: str | None = Field(None, max_length=500)
|
||||
due_date: datetime | None = None
|
||||
_ensure_utc_due = field_validator("due_date")(lambda cls, v: _ensure_utc(v))
|
||||
priority: str | None = Field(None, pattern="^(low|medium|high)$")
|
||||
status: str | None = Field(None, pattern="^(open|in_progress|done|cancelled)$")
|
||||
assigned_to: str | None = None
|
||||
reminder: dict[str, Any] | None = None
|
||||
recurrence: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class EntryResponse(BaseModel):
|
||||
id: str
|
||||
calendar_id: str
|
||||
entry_type: str
|
||||
subtype: str
|
||||
title: str
|
||||
description: str | None = None
|
||||
start_at: datetime | None = None
|
||||
end_at: datetime | None = None
|
||||
all_day: bool
|
||||
location: str | None = None
|
||||
due_date: datetime | None = None
|
||||
priority: str
|
||||
status: str
|
||||
assigned_to: str | None = None
|
||||
reminder: dict[str, Any] | None = None
|
||||
recurrence: dict[str, Any] | None = None
|
||||
created_by: str
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
links: list[dict] = []
|
||||
subtasks: list[dict] = []
|
||||
|
||||
|
||||
class LinkRequest(BaseModel):
|
||||
entity_type: str = Field(..., pattern="^(company|contact)$")
|
||||
entity_id: str
|
||||
|
||||
|
||||
class SubtaskCreate(BaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=500)
|
||||
|
||||
|
||||
class SubtaskResponse(BaseModel):
|
||||
id: str
|
||||
entry_id: str
|
||||
title: str
|
||||
completed: bool
|
||||
|
||||
|
||||
class SubtaskUpdate(BaseModel):
|
||||
completed: bool | None = None
|
||||
title: str | None = Field(None, min_length=1, max_length=500)
|
||||
|
||||
|
||||
class BulkAction(BaseModel):
|
||||
entry_ids: list[str] = Field(..., min_length=1)
|
||||
action: str = Field(..., pattern="^(open|in_progress|done|cancelled|delete)$")
|
||||
|
||||
|
||||
class ResourceCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=200)
|
||||
type: str = Field(..., pattern="^(room|equipment)$")
|
||||
|
||||
|
||||
class ResourceResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
|
||||
|
||||
class BookResourceRequest(BaseModel):
|
||||
resource_id: str
|
||||
|
||||
|
||||
class ResourceBookingResponse(BaseModel):
|
||||
id: str
|
||||
resource_id: str
|
||||
entry_id: str
|
||||
start_at: datetime
|
||||
end_at: datetime
|
||||
|
||||
|
||||
class ImportResult(BaseModel):
|
||||
entries_created: int
|
||||
errors: list[str] = []
|
||||
Reference in New Issue
Block a user