CalDAV account connect (Settings → Calendars) with RFC 6764 discovery, per-calendar vaulted credentials, and full two-way sync verified live against NextCloud: - caldav.py: discovery, calendar-query REPORT (pull), PUT/DELETE with quoted ETags (If-Match/If-None-Match), iCloud partition-redirect handling, ICS build/parse; clear error codes (auth/private-host/ unreachable) surfaced to the connect UI. - sync.py: ctag-gated pull + push of local changes; dirty rows protected from being clobbered; 15-min cron + immediate enqueue on every create/edit/delete so changes reach the server in ~1s. - Dirty tracking via a BEFORE INSERT/UPDATE trigger (migration 0007), suppressed during sync with a txn-local GUC; deletes tombstone then DELETE server-side. Writability now driven by server privileges. - migration 0006: event uniqueness NULLS NOT DISTINCT — fixes duplicate events on every full re-pull (NULL recurrence_id never matched ON CONFLICT). - 5 CalDAV parsing unit tests. Calendar UI (page.js): all-day events now render in Day/3-days/Week views (were month-only). They share the timed-event column layout so overlapping events sit side-by-side, fill the day column vertically with a sticky label, and are matched by start day (fixes UTC-midnight events spilling into the next local day). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
2.2 KiB
Python
81 lines
2.2 KiB
Python
"""Request models for the calendar API (SPEC §A)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class CalendarIn(BaseModel):
|
|
name: str
|
|
color: str = "#3b82f6"
|
|
|
|
|
|
class CalendarPatch(BaseModel):
|
|
name: str | None = None
|
|
color: str | None = None
|
|
publish: str | None = None # "off" | "full" | "busy" — public .ics feed control
|
|
|
|
|
|
class SubscribeIn(BaseModel):
|
|
url: str # source .ics URL to subscribe to (read-only external calendar)
|
|
name: str = "" # optional calendar name (defaults to the feed's title / host)
|
|
color: str = "#6b7280"
|
|
|
|
|
|
class DavConnectIn(BaseModel):
|
|
provider: str # "icloud" | "caldav"
|
|
server_url: str = "" # required for generic caldav; ignored for known providers
|
|
username: str
|
|
password: str
|
|
|
|
|
|
class Attendee(BaseModel):
|
|
email: str
|
|
name: str = ""
|
|
# RSVP status: needs-action | accepted | declined | tentative
|
|
partstat: str = "needs-action"
|
|
|
|
|
|
class EventIn(BaseModel):
|
|
calendar_id: uuid.UUID
|
|
summary: str = ""
|
|
description: str = ""
|
|
location: str = ""
|
|
dtstart: datetime
|
|
dtend: datetime | None = None
|
|
all_day: bool = False
|
|
tz: str = "UTC"
|
|
rrule: str | None = None
|
|
reminders: list[int] = []
|
|
attendees: list[Attendee] = [] # stored in A1; iMIP invites are sent in A2
|
|
organizer: Attendee | None = None
|
|
color: str | None = None # None = inherit the calendar colour
|
|
show_as: str = "busy" # busy | free
|
|
conference_url: str = "" # Jitsi Meet video-call link; empty = none
|
|
|
|
|
|
class EditFollowingIn(BaseModel):
|
|
# ``recurrence_start`` = the clicked occurrence's original start; ``event`` = the
|
|
# edited fields. Splits the series so occurrences BEFORE it are untouched.
|
|
recurrence_start: datetime
|
|
event: EventIn
|
|
|
|
|
|
class EventPatch(BaseModel):
|
|
summary: str | None = None
|
|
description: str | None = None
|
|
location: str | None = None
|
|
dtstart: datetime | None = None
|
|
dtend: datetime | None = None
|
|
all_day: bool | None = None
|
|
tz: str | None = None
|
|
rrule: str | None = None
|
|
reminders: list[int] | None = None
|
|
attendees: list[Attendee] | None = None
|
|
color: str | None = None
|
|
show_as: str | None = None
|
|
conference_url: str | None = None
|